diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2024-02-17 03:08:25 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2024-02-17 03:08:25 +0300 |
commit | 0ce2e41a2bad790c5232fafb4b6ed631ca8cd957 (patch) | |
tree | fd401495b87cae8c95a4c4edf2c851c8177b6069 | |
parent | e9fc2c1835f7ac8e072919df81a6661c6308dea9 (diff) | |
parent | b7f1d55c9b4de4d21b11e5615a5dc8be0d4e883c (diff) |
merge with master
-rw-r--r-- | .gitignore | 13 | ||||
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | arduino/ESP32CameraWebServer/CameraWebServer.ino (renamed from esp32-cam/CameraWebServer/CameraWebServer.ino) | 0 | ||||
-rw-r--r-- | arduino/ESP32CameraWebServer/app_httpd.cpp (renamed from esp32-cam/CameraWebServer/app_httpd.cpp) | 0 | ||||
-rw-r--r-- | arduino/ESP32CameraWebServer/camera_index.h (renamed from esp32-cam/CameraWebServer/camera_index.h) | 0 | ||||
-rw-r--r-- | arduino/ESP32CameraWebServer/camera_pins.h (renamed from esp32-cam/CameraWebServer/camera_pins.h) | 0 | ||||
-rw-r--r-- | arduino/ESP32CameraWebServer/index_ov2640.html (renamed from esp32-cam/CameraWebServer/index_ov2640.html) | 0 | ||||
-rw-r--r-- | bin/__py_include.py | 9 | ||||
-rwxr-xr-x | bin/camera_node.py (renamed from src/camera_node.py) | 11 | ||||
-rwxr-xr-x | bin/electricity_calc.py (renamed from src/electricity_calc.py) | 6 | ||||
-rwxr-xr-x | bin/esp32_capture.py (renamed from src/esp32_capture.py) | 7 | ||||
-rwxr-xr-x | bin/esp32cam_capture_diff_node.py (renamed from src/esp32cam_capture_diff_node.py) | 13 | ||||
-rwxr-xr-x | bin/gpiorelayd.py | 31 | ||||
-rwxr-xr-x | bin/inverter_bot.py (renamed from src/inverter_bot.py) | 269 | ||||
-rwxr-xr-x | bin/inverter_mqtt_util.py (renamed from src/inverter_mqtt_util.py) | 10 | ||||
-rwxr-xr-x | bin/inverterd_emulator.py (renamed from src/inverterd_emulator.py) | 3 | ||||
-rwxr-xr-x | bin/ipcam_capture.py | 142 | ||||
-rwxr-xr-x | bin/ipcam_motion_worker.sh (renamed from tools/ipcam_motion_worker.sh) | 2 | ||||
-rwxr-xr-x | bin/ipcam_ntp_util.py | 199 | ||||
-rwxr-xr-x | bin/ipcam_server.py (renamed from src/ipcam_server.py) | 225 | ||||
-rwxr-xr-x | bin/lugovaya_pump_mqtt_bot.py | 207 | ||||
-rwxr-xr-x | bin/mqtt_node_util.py (renamed from src/mqtt_node_util.py) | 64 | ||||
-rwxr-xr-x | bin/openwrt_log_analyzer.py | 79 | ||||
-rwxr-xr-x | bin/openwrt_logger.py (renamed from src/openwrt_logger.py) | 40 | ||||
-rw-r--r-- | bin/pio_build.py (renamed from src/pio_build.py) | 1 | ||||
-rwxr-xr-x | bin/pio_ini.py (renamed from src/pio_ini.py) | 30 | ||||
-rwxr-xr-x | bin/polaris_kettle_bot.py (renamed from src/polaris_kettle_bot.py) | 13 | ||||
-rwxr-xr-x | bin/polaris_kettle_util.py (renamed from src/polaris_kettle_util.py) | 5 | ||||
-rwxr-xr-x | bin/pump_bot.py (renamed from src/pump_bot.py) | 169 | ||||
-rwxr-xr-x | bin/pump_mqtt_bot.py (renamed from src/pump_mqtt_bot.py) | 14 | ||||
-rwxr-xr-x | bin/relay_mqtt_bot.py (renamed from src/relay_mqtt_bot.py) | 76 | ||||
-rwxr-xr-x | bin/relay_mqtt_http_proxy.py | 134 | ||||
-rwxr-xr-x | bin/sensors_bot.py (renamed from src/sensors_bot.py) | 17 | ||||
-rwxr-xr-x | bin/sound_bot.py (renamed from src/sound_bot.py) | 31 | ||||
-rwxr-xr-x | bin/sound_node.py (renamed from src/sound_node.py) | 9 | ||||
-rwxr-xr-x | bin/sound_sensor_node.py (renamed from src/sound_sensor_node.py) | 9 | ||||
-rwxr-xr-x | bin/sound_sensor_server.py (renamed from src/sound_sensor_server.py) | 21 | ||||
-rwxr-xr-x | bin/ssh_tunnels_config_util.py (renamed from src/ssh_tunnels_config_util.py) | 6 | ||||
-rwxr-xr-x | bin/temphum_mqtt_node.py (renamed from src/temphum_mqtt_node.py) | 7 | ||||
-rwxr-xr-x | bin/temphum_mqtt_receiver.py (renamed from src/temphum_mqtt_receiver.py) | 8 | ||||
-rwxr-xr-x | bin/temphum_nodes_util.py (renamed from src/temphum_nodes_util.py) | 4 | ||||
-rwxr-xr-x | bin/temphum_smbus_util.py (renamed from src/temphum_smbus_util.py) | 6 | ||||
-rwxr-xr-x | bin/temphumd.py (renamed from src/temphumd.py) | 7 | ||||
-rwxr-xr-x | bin/web_api.py (renamed from src/web_api.py) | 38 | ||||
-rw-r--r-- | bin/web_kbn.py | 354 | ||||
-rw-r--r-- | doc/openwrt_logger.md | 28 | ||||
-rw-r--r-- | include/bash/include.bash (renamed from tools/lib.bash) | 0 | ||||
-rw-r--r-- | include/pio/include/homekit/logging.h (renamed from platformio/common/include/homekit/logging.h) | 0 | ||||
-rw-r--r-- | include/pio/include/homekit/macros.h (renamed from platformio/common/include/homekit/macros.h) | 0 | ||||
-rw-r--r-- | include/pio/include/homekit/stopwatch.h (renamed from platformio/common/include/homekit/stopwatch.h) | 0 | ||||
-rw-r--r-- | include/pio/include/homekit/util.h (renamed from platformio/common/include/homekit/util.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/config/homekit/config.cpp (renamed from platformio/common/libs/config/homekit/config.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/config/homekit/config.h (renamed from platformio/common/libs/config/homekit/config.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/config/library.json (renamed from platformio/common/libs/config/library.json) | 0 | ||||
-rw-r--r-- | include/pio/libs/http_server/homekit/http_server.cpp (renamed from platformio/common/libs/http_server/homekit/http_server.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/http_server/homekit/http_server.h (renamed from platformio/common/libs/http_server/homekit/http_server.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/http_server/library.json (renamed from platformio/common/libs/http_server/library.json) | 0 | ||||
-rw-r--r-- | include/pio/libs/led/homekit/led.cpp (renamed from platformio/common/libs/led/homekit/led.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/led/homekit/led.h (renamed from platformio/common/libs/led/homekit/led.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/led/library.json (renamed from platformio/common/libs/led/library.json) | 0 | ||||
-rw-r--r-- | include/pio/libs/main/homekit/main.cpp (renamed from platformio/common/libs/main/homekit/main.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/main/homekit/main.h (renamed from platformio/common/libs/main/homekit/main.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/main/library.json | 12 | ||||
-rw-r--r-- | include/pio/libs/mqtt/homekit/mqtt/module.cpp (renamed from platformio/common/libs/mqtt/homekit/mqtt/module.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt/homekit/mqtt/module.h (renamed from platformio/common/libs/mqtt/homekit/mqtt/module.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp (renamed from platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp) | 2 | ||||
-rw-r--r-- | include/pio/libs/mqtt/homekit/mqtt/mqtt.h (renamed from platformio/common/libs/mqtt/homekit/mqtt/mqtt.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt/homekit/mqtt/payload.h (renamed from platformio/common/libs/mqtt/homekit/mqtt/payload.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt/library.json (renamed from platformio/common/libs/mqtt/library.json) | 2 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp (renamed from platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h (renamed from platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_diagnostics/library.json (renamed from platformio/common/libs/mqtt_module_diagnostics/library.json) | 4 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp (renamed from platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h (renamed from platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_ota/library.json | 11 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp (renamed from platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h (renamed from platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_relay/library.json | 11 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp (renamed from platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h (renamed from platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/mqtt_module_temphum/library.json (renamed from platformio/common/libs/mqtt_module_temphum/library.json) | 4 | ||||
-rw-r--r-- | include/pio/libs/relay/homekit/relay.cpp (renamed from platformio/common/libs/relay/homekit/relay.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/relay/homekit/relay.h (renamed from platformio/common/libs/relay/homekit/relay.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/relay/library.json (renamed from platformio/common/libs/relay/library.json) | 0 | ||||
-rw-r--r-- | include/pio/libs/static/homekit/static.cpp (renamed from platformio/common/libs/static/homekit/static.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/static/homekit/static.h (renamed from platformio/common/libs/static/homekit/static.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/static/library.json (renamed from platformio/common/libs/static/library.json) | 0 | ||||
-rw-r--r-- | include/pio/libs/temphum/homekit/temphum.cpp (renamed from platformio/common/libs/temphum/homekit/temphum.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/temphum/homekit/temphum.h (renamed from platformio/common/libs/temphum/homekit/temphum.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/temphum/library.json (renamed from platformio/common/libs/temphum/library.json) | 2 | ||||
-rw-r--r-- | include/pio/libs/wifi/homekit/wifi.cpp (renamed from platformio/common/libs/wifi/homekit/wifi.cpp) | 0 | ||||
-rw-r--r-- | include/pio/libs/wifi/homekit/wifi.h (renamed from platformio/common/libs/wifi/homekit/wifi.h) | 0 | ||||
-rw-r--r-- | include/pio/libs/wifi/library.json (renamed from platformio/common/libs/wifi/library.json) | 0 | ||||
-rwxr-xr-x | include/pio/make_static.sh (renamed from platformio/common/make_static.sh) | 0 | ||||
-rw-r--r-- | include/pio/static/app.js (renamed from platformio/common/static/app.js) | 0 | ||||
-rw-r--r-- | include/pio/static/favicon.ico (renamed from platformio/common/static/favicon.ico) | bin | 7886 -> 7886 bytes | |||
-rw-r--r-- | include/pio/static/index.html (renamed from platformio/common/static/index.html) | 0 | ||||
-rw-r--r-- | include/pio/static/md5.js (renamed from platformio/common/static/md5.js) | 0 | ||||
-rw-r--r-- | include/pio/static/style.css (renamed from platformio/common/static/style.css) | 0 | ||||
-rw-r--r-- | include/py/__init__.py (renamed from src/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/__init__.py (renamed from src/home/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/api/__init__.py | 19 | ||||
-rw-r--r-- | include/py/homekit/api/__init__.pyi | 5 | ||||
-rw-r--r-- | include/py/homekit/api/config.py | 15 | ||||
-rw-r--r-- | include/py/homekit/api/errors/__init__.py (renamed from src/home/api/errors/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/api/errors/api_response_error.py (renamed from src/home/api/errors/api_response_error.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/api/types/__init__.py (renamed from src/home/api/types/__init__.py) | 1 | ||||
-rw-r--r-- | include/py/homekit/api/types/types.py (renamed from src/home/api/types/types.py) | 11 | ||||
-rw-r--r-- | include/py/homekit/api/web_api_client.py (renamed from src/home/api/web_api_client.py) | 42 | ||||
-rw-r--r-- | include/py/homekit/audio/__init__.py (renamed from src/home/audio/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/audio/amixer.py (renamed from src/home/audio/amixer.py) | 12 | ||||
-rw-r--r-- | include/py/homekit/camera/__init__.py | 2 | ||||
-rw-r--r-- | include/py/homekit/camera/config.py | 141 | ||||
-rw-r--r-- | include/py/homekit/camera/esp32.py (renamed from src/home/camera/esp32.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/camera/types.py | 58 | ||||
-rw-r--r-- | include/py/homekit/camera/util.py (renamed from src/home/camera/util.py) | 70 | ||||
-rw-r--r-- | include/py/homekit/config/__init__.py (renamed from src/home/config/__init__.py) | 4 | ||||
-rw-r--r-- | include/py/homekit/config/_configs.py (renamed from src/home/config/_configs.py) | 15 | ||||
-rw-r--r-- | include/py/homekit/config/config.py (renamed from src/home/config/config.py) | 174 | ||||
-rw-r--r-- | include/py/homekit/database/__init__.py (renamed from src/home/database/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/__init__.pyi (renamed from src/home/database/__init__.pyi) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/_base.py | 9 | ||||
-rw-r--r-- | include/py/homekit/database/bots.py (renamed from src/home/database/bots.py) | 10 | ||||
-rw-r--r-- | include/py/homekit/database/clickhouse.py (renamed from src/home/database/clickhouse.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/inverter.py (renamed from src/home/database/inverter.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/inverter_time_formats.py (renamed from src/home/database/inverter_time_formats.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/mysql.py (renamed from src/home/database/mysql.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/sensors.py (renamed from src/home/database/sensors.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/database/simple_state.py (renamed from src/home/database/simple_state.py) | 14 | ||||
-rw-r--r-- | include/py/homekit/database/sqlite.py (renamed from src/home/database/sqlite.py) | 22 | ||||
-rw-r--r-- | include/py/homekit/http/__init__.py | 2 | ||||
-rw-r--r-- | include/py/homekit/http/http.py (renamed from src/home/http/http.py) | 11 | ||||
-rw-r--r-- | include/py/homekit/inverter/__init__.py (renamed from src/home/inverter/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/inverter/config.py | 13 | ||||
-rw-r--r-- | include/py/homekit/inverter/emulator.py (renamed from src/home/inverter/emulator.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/inverter/inverter_wrapper.py (renamed from src/home/inverter/inverter_wrapper.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/inverter/monitor.py (renamed from src/home/inverter/monitor.py) | 2 | ||||
-rw-r--r-- | include/py/homekit/inverter/types.py (renamed from src/home/inverter/types.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/inverter/util.py (renamed from src/home/inverter/util.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/__init__.py (renamed from src/home/media/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/__init__.pyi (renamed from src/home/media/__init__.pyi) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/node_client.py (renamed from src/home/media/node_client.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/node_server.py (renamed from src/home/media/node_server.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/record.py (renamed from src/home/media/record.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/record_client.py (renamed from src/home/media/record_client.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/storage.py (renamed from src/home/media/storage.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/media/types.py (renamed from src/home/media/types.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/modem/__init__.py | 2 | ||||
-rw-r--r-- | include/py/homekit/modem/config.py | 29 | ||||
-rw-r--r-- | include/py/homekit/modem/e3372.py | 253 | ||||
-rw-r--r-- | include/py/homekit/mqtt/__init__.py (renamed from src/home/mqtt/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_config.py (renamed from src/home/mqtt/_config.py) | 33 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_module.py (renamed from src/home/mqtt/_module.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_mqtt.py (renamed from src/home/mqtt/_mqtt.py) | 6 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_node.py (renamed from src/home/mqtt/_node.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_payload.py (renamed from src/home/mqtt/_payload.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_util.py (renamed from src/home/mqtt/_util.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/_wrapper.py (renamed from src/home/mqtt/_wrapper.py) | 26 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/diagnostics.py (renamed from src/home/mqtt/module/diagnostics.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/inverter.py (renamed from src/home/mqtt/module/inverter.py) | 2 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/ota.py (renamed from src/home/mqtt/module/ota.py) | 2 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/relay.py (renamed from src/home/mqtt/module/relay.py) | 19 | ||||
-rw-r--r-- | include/py/homekit/mqtt/module/temphum.py (renamed from src/home/mqtt/module/temphum.py) | 39 | ||||
-rw-r--r-- | include/py/homekit/pio/__init__.py (renamed from src/home/pio/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/pio/exceptions.py (renamed from src/home/pio/exceptions.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/pio/products.py (renamed from src/home/pio/products.py) | 20 | ||||
-rw-r--r-- | include/py/homekit/relay/__init__.py (renamed from src/home/relay/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/relay/__init__.pyi (renamed from src/home/relay/__init__.pyi) | 0 | ||||
-rw-r--r-- | include/py/homekit/relay/sunxi_h3_client.py (renamed from src/home/relay/sunxi_h3_client.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/relay/sunxi_h3_server.py (renamed from src/home/relay/sunxi_h3_server.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/__init__.py (renamed from src/home/soundsensor/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/__init__.pyi (renamed from src/home/soundsensor/__init__.pyi) | 0 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/node.py (renamed from src/home/soundsensor/node.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/server.py (renamed from src/home/soundsensor/server.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/soundsensor/server_client.py (renamed from src/home/soundsensor/server_client.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/telegram/__init__.py (renamed from src/home/telegram/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/telegram/_botcontext.py (renamed from src/home/telegram/_botcontext.py) | 19 | ||||
-rw-r--r-- | include/py/homekit/telegram/_botdb.py (renamed from src/home/telegram/_botdb.py) | 2 | ||||
-rw-r--r-- | include/py/homekit/telegram/_botlang.py (renamed from src/home/telegram/_botlang.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/telegram/_botutil.py (renamed from src/home/telegram/_botutil.py) | 17 | ||||
-rw-r--r-- | include/py/homekit/telegram/aio.py (renamed from src/home/telegram/aio.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/telegram/bot.py (renamed from src/home/telegram/bot.py) | 242 | ||||
-rw-r--r-- | include/py/homekit/telegram/config.py (renamed from src/home/telegram/config.py) | 27 | ||||
-rw-r--r-- | include/py/homekit/telegram/telegram.py (renamed from src/home/telegram/telegram.py) | 28 | ||||
-rw-r--r-- | include/py/homekit/temphum/__init__.py (renamed from src/home/temphum/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/temphum/base.py (renamed from src/home/temphum/base.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/temphum/i2c.py (renamed from src/home/temphum/i2c.py) | 0 | ||||
-rw-r--r-- | include/py/homekit/util.py (renamed from src/home/util.py) | 135 | ||||
-rw-r--r-- | include/py/pyA20/__init__.pyi (renamed from pyA20/__init__.pyi) | 0 | ||||
-rw-r--r-- | include/py/pyA20/gpio/connector.pyi (renamed from pyA20/gpio/connector.pyi) | 0 | ||||
-rw-r--r-- | include/py/pyA20/gpio/gpio.pyi (renamed from pyA20/gpio/gpio.pyi) | 0 | ||||
-rw-r--r-- | include/py/pyA20/gpio/port.pyi (renamed from pyA20/gpio/port.pyi) | 0 | ||||
-rw-r--r-- | include/py/pyA20/port.pyi (renamed from pyA20/port.pyi) | 0 | ||||
-rw-r--r-- | include/py/syncleo/__init__.py (renamed from src/syncleo/__init__.py) | 0 | ||||
-rw-r--r-- | include/py/syncleo/kettle.py (renamed from src/syncleo/kettle.py) | 0 | ||||
-rw-r--r-- | include/py/syncleo/protocol.py (renamed from src/syncleo/protocol.py) | 0 | ||||
-rw-r--r-- | localwebsite/classes/E3372.php | 310 | ||||
-rw-r--r-- | localwebsite/classes/GPIORelaydClient.php | 18 | ||||
-rw-r--r-- | localwebsite/classes/InverterdClient.php | 69 | ||||
-rw-r--r-- | localwebsite/classes/MyOpenWrtUtils.php | 10 | ||||
-rw-r--r-- | localwebsite/handlers/InverterHandler.php | 102 | ||||
-rw-r--r-- | localwebsite/handlers/MiscHandler.php | 41 | ||||
-rw-r--r-- | localwebsite/handlers/ModemHandler.php | 197 | ||||
-rw-r--r-- | localwebsite/htdocs/assets/inverter.js | 15 | ||||
-rw-r--r-- | localwebsite/htdocs/assets/modem.js | 29 | ||||
-rw-r--r-- | localwebsite/htdocs/index.php | 9 | ||||
-rw-r--r-- | localwebsite/templates-web/index.twig | 6 | ||||
-rw-r--r-- | localwebsite/templates-web/inverter_page.twig | 20 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_data.twig | 14 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_status_page.twig | 19 | ||||
-rw-r--r-- | localwebsite/templates-web/modem_verbose_page.twig | 15 | ||||
-rw-r--r-- | localwebsite/templates-web/routing_header.twig | 2 | ||||
-rw-r--r-- | localwebsite/templates-web/spinner.twig | 14 | ||||
-rw-r--r-- | misc/home_linux_boards/etc/default/homekit_ipcam_server | 2 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/home_linux_boards/usr/local/bin/homekit_ipcam_capture_restart.sh (renamed from misc/scripts/ipcam_capture_restart.sh) | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/home_linux_boards/usr/local/bin/homekit_ipcam_rtsp2hls_restart.sh (renamed from misc/scripts/ipcam_rtsp2hls_restart.sh) | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/home_linux_boards/usr/local/bin/homekit_make_netns_per_upstream.sh (renamed from misc/scripts/make_netns_per_upstream.sh) | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/home_linux_boards/usr/local/bin/homekit_sunxi_h3_i2c_reset.sh (renamed from tools/sunxi-h3-i2c-reset.sh) | 0 | ||||
-rwxr-xr-x | misc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh (renamed from tools/sunxi-setup-amixer.sh) | 0 | ||||
-rwxr-xr-x | misc/home_linux_boards/usr/local/bin/homekit_sync_recordings_to_remote.sh (renamed from tools/sync-recordings-to-remote.sh) | 0 | ||||
-rw-r--r-- | misc/mqtt_ca.crt (renamed from assets/mqtt_ca.crt) | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/remote_server/usr/local/bin/clickhouse_backup.sh (renamed from tools/clickhouse-backup.sh) | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | misc/remote_server/usr/local/bin/remove_old_recordings.sh (renamed from tools/remove-old-recordings.sh) | 0 | ||||
-rw-r--r-- | pio/dumb_mqtt/src/main.cpp (renamed from platformio/dumb_mqtt/src/main.cpp) | 0 | ||||
-rw-r--r-- | pio/relayctl/src/main.cpp (renamed from platformio/relayctl/src/main.cpp) | 0 | ||||
-rw-r--r-- | pio/temphum/src/main.cpp (renamed from platformio/temphum/src/main.cpp) | 0 | ||||
-rw-r--r-- | pio/temphum_relayctl/src/main.cpp (renamed from platformio/temphum_relayctl/src/main.cpp) | 0 | ||||
-rw-r--r-- | platformio/common/libs/main/library.json | 12 | ||||
-rw-r--r-- | platformio/common/libs/mqtt_module_ota/library.json | 11 | ||||
-rw-r--r-- | platformio/common/libs/mqtt_module_relay/library.json | 11 | ||||
-rw-r--r-- | platformio/dumb_mqtt/.gitignore | 3 | ||||
-rw-r--r-- | platformio/relayctl/.gitignore | 3 | ||||
-rw-r--r-- | platformio/temphum/.gitignore | 3 | ||||
-rw-r--r-- | requirements.txt | 22 | ||||
-rw-r--r-- | requirements_kettle.txt | 3 | ||||
-rwxr-xr-x | src/gpiorelayd.py | 23 | ||||
-rw-r--r-- | src/home/api/__init__.py | 11 | ||||
-rw-r--r-- | src/home/api/__init__.pyi | 4 | ||||
-rw-r--r-- | src/home/camera/__init__.py | 1 | ||||
-rw-r--r-- | src/home/camera/types.py | 5 | ||||
-rw-r--r-- | src/home/http/__init__.py | 2 | ||||
-rw-r--r-- | src/home/inverter/config.py | 13 | ||||
-rwxr-xr-x | src/openwrt_log_analyzer.py | 72 | ||||
-rwxr-xr-x | src/relay_mqtt_http_proxy.py | 81 | ||||
-rwxr-xr-x | src/test_new_config.py | 11 | ||||
-rw-r--r-- | systemd/camera_node.service | 2 | ||||
-rw-r--r-- | systemd/camera_node@.service | 2 | ||||
-rw-r--r-- | systemd/esp32cam_capture_diff_node.service | 2 | ||||
-rw-r--r-- | systemd/gpiorelayd@.service | 5 | ||||
-rw-r--r-- | systemd/inverter_bot.service | 2 | ||||
-rw-r--r-- | systemd/inverter_mqtt_receiver.service | 4 | ||||
-rw-r--r-- | systemd/inverter_mqtt_sender.service | 4 | ||||
-rw-r--r-- | systemd/ipcam_capture@.service | 15 | ||||
-rw-r--r-- | systemd/ipcam_rtsp2hls@.service | 16 | ||||
-rw-r--r-- | systemd/ipcam_server.service | 5 | ||||
-rw-r--r-- | systemd/polaris_kettle_bot.service | 2 | ||||
-rw-r--r-- | systemd/pump_bot.service | 2 | ||||
-rw-r--r-- | systemd/pump_mqtt_bot.service | 2 | ||||
-rw-r--r-- | systemd/relay_mqtt_bot.service | 2 | ||||
-rw-r--r-- | systemd/relay_mqtt_http_proxy.service | 2 | ||||
-rw-r--r-- | systemd/sensors_bot.service | 2 | ||||
-rw-r--r-- | systemd/sound_bot.service | 2 | ||||
-rw-r--r-- | systemd/sound_node.service | 2 | ||||
-rw-r--r-- | systemd/sound_sensor_node.service | 2 | ||||
-rw-r--r-- | systemd/sound_sensor_server.service | 2 | ||||
-rw-r--r-- | systemd/temphumd.service | 2 | ||||
-rw-r--r-- | systemd/temphumd@.service | 2 | ||||
-rw-r--r-- | tasks/df_h.sh | 2 | ||||
-rw-r--r-- | test/__init__.py | 0 | ||||
-rw-r--r-- | test/__py_include.py | 9 | ||||
-rwxr-xr-x | test/mqtt_relay_server_util.py | 19 | ||||
-rwxr-xr-x | test/mqtt_relay_util.py | 15 | ||||
-rwxr-xr-x | test/test.py | 7 | ||||
-rwxr-xr-x | test/test_amixer.py | 9 | ||||
-rwxr-xr-x | test/test_api.py | 17 | ||||
-rwxr-xr-x | test/test_esp32_cam.py | 16 | ||||
-rwxr-xr-x | test/test_inverter_monitor.py | 19 | ||||
-rw-r--r-- | test/test_ipcam_server_cleanup.py | 14 | ||||
-rwxr-xr-x | test/test_modems.py | 9 | ||||
-rwxr-xr-x | test/test_polaris_stuff.py | 11 | ||||
-rwxr-xr-x | test/test_record_upload.py | 21 | ||||
-rwxr-xr-x | test/test_send_fake_sound_hit.py | 12 | ||||
-rwxr-xr-x | test/test_sensors_plot.py | 0 | ||||
-rwxr-xr-x | test/test_sound_node_client.py | 9 | ||||
-rwxr-xr-x | test/test_sound_server_api.py | 16 | ||||
-rwxr-xr-x | test/test_stopwatch.py | 3 | ||||
-rw-r--r-- | test/test_telegram_aio_send_photo.py | 13 | ||||
-rwxr-xr-x | tools/ipcam_capture.sh | 119 | ||||
-rwxr-xr-x | tools/ipcam_rtsp2hls.sh | 127 | ||||
-rwxr-xr-x | tools/process-motion-timecodes.py | 61 | ||||
-rwxr-xr-x | tools/rotate-video.sh | 2 | ||||
-rwxr-xr-x | tools/video-util.sh | 2 | ||||
-rw-r--r-- | web/kbn_assets/app.css (renamed from localwebsite/htdocs/assets/app.css) | 2 | ||||
-rw-r--r-- | web/kbn_assets/app.js (renamed from localwebsite/htdocs/assets/app.js) | 51 | ||||
-rw-r--r-- | web/kbn_assets/bootstrap.min.css (renamed from localwebsite/htdocs/assets/bootstrap.min.css) | 0 | ||||
-rw-r--r-- | web/kbn_assets/bootstrap.min.js (renamed from localwebsite/htdocs/assets/bootstrap.min.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm) | bin | 2190151 -> 2190151 bytes | |||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-120func.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm) | bin | 2108889 -> 2108889 bytes | |||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-256mb.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm) | bin | 2108889 -> 2108889 bytes | |||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-512mb.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-format.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-format.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-v20221120.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile-v20221120.wasm (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm) | bin | 2108891 -> 2108891 bytes | |||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/missile.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/missile.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/raw-parser.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/worker-fetch-dist.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/h265webjs-dist/worker-parse-dist.js (renamed from localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/hls.js (renamed from localwebsite/htdocs/assets/hls.js) | 0 | ||||
-rw-r--r-- | web/kbn_assets/polyfills.js (renamed from localwebsite/htdocs/assets/polyfills.js) | 0 | ||||
-rw-r--r-- | web/kbn_templates/base.j2 | 44 | ||||
-rw-r--r-- | web/kbn_templates/index.j2 | 39 | ||||
-rw-r--r-- | web/kbn_templates/inverter.j2 | 20 | ||||
-rw-r--r-- | web/kbn_templates/loading.j2 | 14 | ||||
-rw-r--r-- | web/kbn_templates/modem_data.j2 | 13 | ||||
-rw-r--r-- | web/kbn_templates/modem_verbose.j2 | 18 | ||||
-rw-r--r-- | web/kbn_templates/modems.j2 | 16 | ||||
-rw-r--r-- | web/kbn_templates/pump.j2 (renamed from localwebsite/templates-web/pump.twig) | 16 | ||||
-rw-r--r-- | web/kbn_templates/signal_level.j2 (renamed from localwebsite/templates-web/signal_level.twig) | 2 | ||||
-rw-r--r-- | web/kbn_templates/sms.j2 (renamed from localwebsite/templates-web/sms_page.twig) | 31 |
326 files changed, 3385 insertions, 2601 deletions
@@ -6,18 +6,19 @@ config.def.h __pycache__ .DS_Store -/src/test/test_inverter_monitor.log +/include/test/test_inverter_monitor.log /youtrack-certificate /cpp -/src/test.py -/esp32-cam/CameraWebServer/wifi_password.h +/test/test.py +/bin/test.py +/arduino/ESP32CameraWebServer/wifi_password.h cmake-build-* .pio platformio.ini CMakeListsPrivate.txt -/platformio/*/CMakeLists.txt -/platformio/*/CMakeListsPrivate.txt -/platformio/*/.gitignore +/pio/*/CMakeLists.txt +/pio/*/CMakeListsPrivate.txt +/pio/*/.gitignore *.swp /localwebsite/vendor @@ -5,12 +5,6 @@ a country house, solving real life tasks. Mostly undocumented. -## TODO - -esp8266/esp32 code: - -- move common stuff to the `commom` directory and use it as a framework - ## License BSD-3c diff --git a/esp32-cam/CameraWebServer/CameraWebServer.ino b/arduino/ESP32CameraWebServer/CameraWebServer.ino index ef589d9..ef589d9 100644 --- a/esp32-cam/CameraWebServer/CameraWebServer.ino +++ b/arduino/ESP32CameraWebServer/CameraWebServer.ino diff --git a/esp32-cam/CameraWebServer/app_httpd.cpp b/arduino/ESP32CameraWebServer/app_httpd.cpp index e397c70..e397c70 100644 --- a/esp32-cam/CameraWebServer/app_httpd.cpp +++ b/arduino/ESP32CameraWebServer/app_httpd.cpp diff --git a/esp32-cam/CameraWebServer/camera_index.h b/arduino/ESP32CameraWebServer/camera_index.h index 5ca12e9..5ca12e9 100644 --- a/esp32-cam/CameraWebServer/camera_index.h +++ b/arduino/ESP32CameraWebServer/camera_index.h diff --git a/esp32-cam/CameraWebServer/camera_pins.h b/arduino/ESP32CameraWebServer/camera_pins.h index e1be287..e1be287 100644 --- a/esp32-cam/CameraWebServer/camera_pins.h +++ b/arduino/ESP32CameraWebServer/camera_pins.h diff --git a/esp32-cam/CameraWebServer/index_ov2640.html b/arduino/ESP32CameraWebServer/index_ov2640.html index 4f3738c..4f3738c 100644 --- a/esp32-cam/CameraWebServer/index_ov2640.html +++ b/arduino/ESP32CameraWebServer/index_ov2640.html diff --git a/bin/__py_include.py b/bin/__py_include.py new file mode 100644 index 0000000..8f98830 --- /dev/null +++ b/bin/__py_include.py @@ -0,0 +1,9 @@ +import sys +import os.path + +for _name in ('include/py',): + sys.path.extend([ + os.path.realpath( + os.path.join(os.path.dirname(os.path.join(__file__)), '..', _name) + ) + ])
\ No newline at end of file diff --git a/src/camera_node.py b/bin/camera_node.py index 3f2c5a4..1485557 100755 --- a/src/camera_node.py +++ b/bin/camera_node.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import asyncio import time +import __py_include -from home.config import config -from home.media import MediaNodeServer, ESP32CameraRecordStorage, CameraRecorder -from home.camera import CameraType, esp32 -from home.util import Addr -from home import http +from homekit.config import config +from homekit.media import MediaNodeServer, ESP32CameraRecordStorage, CameraRecorder +from homekit.camera import CameraType, esp32 +from homekit.util import Addr +from homekit import http # Implements HTTP API for a camera. diff --git a/src/electricity_calc.py b/bin/electricity_calc.py index c3cb233..cff2327 100755 --- a/src/electricity_calc.py +++ b/bin/electricity_calc.py @@ -3,12 +3,12 @@ import logging import os import sys import inspect -import zoneinfo +import __py_include -from home.config import config # do not remove this import! +from homekit.config import config # do not remove this import! from datetime import datetime, timedelta from logging import Logger -from home.database import InverterDatabase +from homekit.database import InverterDatabase from argparse import ArgumentParser, ArgumentError from typing import Optional diff --git a/src/esp32_capture.py b/bin/esp32_capture.py index 4a9ce10..839114d 100755 --- a/src/esp32_capture.py +++ b/bin/esp32_capture.py @@ -2,10 +2,11 @@ import asyncio import logging import os.path +import __py_include from argparse import ArgumentParser -from home.camera.esp32 import WebClient -from home.util import parse_addr, Addr +from homekit.camera.esp32 import WebClient +from homekit.util import Addr from apscheduler.schedulers.asyncio import AsyncIOScheduler from datetime import datetime from typing import Optional @@ -50,7 +51,7 @@ if __name__ == '__main__': loop = asyncio.get_event_loop() - ESP32Capture(parse_addr(arg.addr), arg.interval, arg.output_directory) + ESP32Capture(Addr.fromstring(arg.addr), arg.interval, arg.output_directory) try: loop.run_forever() except KeyboardInterrupt: diff --git a/src/esp32cam_capture_diff_node.py b/bin/esp32cam_capture_diff_node.py index 70ebd47..d664c6d 100755 --- a/src/esp32cam_capture_diff_node.py +++ b/bin/esp32cam_capture_diff_node.py @@ -3,11 +3,12 @@ import asyncio import logging import os.path import tempfile -import home.telegram.aio as telegram +import __py_include +import homekit.telegram.aio as telegram -from home.config import config -from home.camera.esp32 import WebClient -from home.util import parse_addr, send_datagram, stringify +from homekit.config import config +from homekit.camera.esp32 import WebClient +from homekit.util import Addr, send_datagram, stringify from apscheduler.schedulers.asyncio import AsyncIOScheduler from typing import Optional @@ -34,11 +35,11 @@ async def pyssim(fn1: str, fn2: str) -> float: class ESP32CamCaptureDiffNode: def __init__(self): - self.client = WebClient(parse_addr(config['esp32cam_web_addr'])) + self.client = WebClient(Addr.fromstring(config['esp32cam_web_addr'])) self.directory = tempfile.gettempdir() self.nextpic = 1 self.first = True - self.server_addr = parse_addr(config['node']['server_addr']) + self.server_addr = Addr.fromstring(config['node']['server_addr']) self.scheduler = AsyncIOScheduler() self.scheduler.add_job(self.capture, 'interval', seconds=config['node']['interval']) diff --git a/bin/gpiorelayd.py b/bin/gpiorelayd.py new file mode 100755 index 0000000..89ba78e --- /dev/null +++ b/bin/gpiorelayd.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import logging +import os +import sys +import __py_include + +from argparse import ArgumentParser +from homekit.util import Addr +from homekit.config import config +from homekit.relay.sunxi_h3_server import RelayServer + +logger = logging.getLogger(__name__) + + +if __name__ == '__main__': + if os.getegid() != 0: + sys.exit('Must be run as root.') + + parser = ArgumentParser() + parser.add_argument('--pin', type=str, required=True, + help='name of GPIO pin of Allwinner H3 sunxi board') + parser.add_argument('--listen', type=str, required=True, + help='address to listen to, in ip:port format') + + arg = config.load_app(no_config=True, parser=parser) + listen = Addr.fromstring(arg.listen) + + try: + RelayServer(pinname=arg.pin, addr=listen).run() + except KeyboardInterrupt: + logger.info('Exiting...') diff --git a/src/inverter_bot.py b/bin/inverter_bot.py index ecf01fc..0be5866 100755 --- a/src/inverter_bot.py +++ b/bin/inverter_bot.py @@ -5,30 +5,31 @@ import datetime import json import itertools import sys +import asyncio +import __py_include from inverterd import Format, InverterError from html import escape from typing import Optional, Tuple, Union -from home.util import chunks -from home.config import config, AppConfigUnit -from home.telegram import bot -from home.telegram.config import TelegramBotConfig, TelegramUserListType -from home.inverter import ( +from homekit.util import chunks +from homekit.config import config, AppConfigUnit +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig, TelegramUserListType +from homekit.inverter import ( wrapper_instance as inverter, beautify_table, InverterMonitor, ) -from home.inverter.types import ( +from homekit.inverter.types import ( ChargingEvent, ACPresentEvent, BatteryState, ACMode, OutputSourcePriority ) -from home.database.inverter_time_formats import FormatDate -from home.api.types import BotType -from home.api import WebAPIClient +from homekit.database.inverter_time_formats import FormatDate +from homekit.api import WebApiClient from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton @@ -55,8 +56,8 @@ logger = logging.getLogger(__name__) class InverterBotConfig(AppConfigUnit, TelegramBotConfig): NAME = 'inverter_bot' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: acmode_item_schema = { 'thresholds': { 'type': 'list', @@ -347,8 +348,11 @@ def monitor_charging(event: ChargingEvent, **kwargs) -> None: key = f'chrg_evt_{key}' if is_util: key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang, *args) + + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang, *args) + ) ) @@ -363,9 +367,11 @@ def monitor_battery(state: BatteryState, v: float, load_watts: int) -> None: logger.error('unknown battery state:', state) return - bot.notify_all( - lambda lang: bot.lang.get('battery_level_changed', lang, - emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('battery_level_changed', lang, + emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + ) ) @@ -375,14 +381,18 @@ def monitor_util(event: ACPresentEvent): else: key = 'disconnected' key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang) + ) ) def monitor_error(error: str) -> None: - bot.notify_all( - lambda lang: bot.lang.get('error_message', lang, error) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('error_message', lang, error) + ) ) @@ -392,35 +402,37 @@ def osp_change_cb(new_osp: OutputSourcePriority, setosp(new_osp) - bot.notify_all( - lambda lang: bot.lang.get('osp_auto_changed_notification', lang, - bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('osp_auto_changed_notification', lang, + bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + ) ) @bot.handler(command='status') -def full_status(ctx: bot.Context) -> None: +async def full_status(ctx: bot.Context) -> None: status = inverter.exec('get-status', format=Format.TABLE) - ctx.reply(beautify_table(status)) + await ctx.reply(beautify_table(status)) @bot.handler(command='config') -def full_rated(ctx: bot.Context) -> None: +async def full_rated(ctx: bot.Context) -> None: rated = inverter.exec('get-rated', format=Format.TABLE) - ctx.reply(beautify_table(rated)) + await ctx.reply(beautify_table(rated)) @bot.handler(command='errors') -def full_errors(ctx: bot.Context) -> None: +async def full_errors(ctx: bot.Context) -> None: errors = inverter.exec('get-errors', format=Format.TABLE) - ctx.reply(beautify_table(errors)) + await ctx.reply(beautify_table(errors)) @bot.handler(command='flags') -def flags_handler(ctx: bot.Context) -> None: +async def flags_handler(ctx: bot.Context) -> None: flags = inverter.exec('get-flags')['data'] text, markup = build_flags_keyboard(flags, ctx) - ctx.reply(text, markup=markup) + await ctx.reply(text, markup=markup) def build_flags_keyboard(flags: dict, ctx: bot.Context) -> Tuple[str, InlineKeyboardMarkup]: @@ -477,11 +489,11 @@ class SettingsConversation(bot.conversation): REDISCHARGE_VOLTAGES = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58] @bot.conventer(START, message='settings') - def start_enter(self, ctx: bot.Context): + async def start_enter(self, ctx: bot.Context): buttons = list(chunks(list(self.START_BUTTONS), 2)) buttons.reverse() - return self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, - with_cancel=True) + return await self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, + with_cancel=True) @bot.convinput(START, messages={ 'settings_osp': OSP, @@ -490,16 +502,16 @@ class SettingsConversation(bot.conversation): 'settings_bat_cut_off_voltage': BAT_CUT_OFF_VOLTAGE, 'settings_ac_max_charging_current': AC_MAX_CHARGING_CURRENT }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(OSP) - def osp_enter(self, ctx: bot.Context): - return self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, - with_back=True) + async def osp_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, + with_back=True) @bot.convinput(OSP, messages=OSP_BUTTONS) - def osp_input(self, ctx: bot.Context): + async def osp_input(self, ctx: bot.Context): selected_sp = None for sp in OutputSourcePriority: if ctx.text == ctx.lang(f'settings_osp_{sp.value.lower()}'): @@ -512,25 +524,28 @@ class SettingsConversation(bot.conversation): # apply the mode setosp(selected_sp) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()) - - # notify other users - bot.notify_all( - lambda lang: bot.lang.get('osp_changed_notification', lang, - ctx.user.id, ctx.user.name, - bot.lang.get(f'settings_osp_{selected_sp.value.lower()}', lang)), - exclude=(ctx.user_id,) + await asyncio.gather( + # reply to user + ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()), + + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('osp_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(f'settings_osp_{selected_sp.value.lower()}', lang)), + exclude=(ctx.user_id,) + ) ) + return self.END @bot.conventer(AC_PRESET) - def acpreset_enter(self, ctx: bot.Context): - return self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, - with_back=True) + async def acpreset_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, + with_back=True) @bot.convinput(AC_PRESET, messages=AC_PRESET_BUTTONS) - def acpreset_input(self, ctx: bot.Context): + async def acpreset_input(self, ctx: bot.Context): if monitor.active_current is not None: raise RuntimeError('generator charging program is active') @@ -547,85 +562,88 @@ class SettingsConversation(bot.conversation): # save bot.db.set_param('ac_mode', str(newmode.value)) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()) - - # notify other users - bot.notify_all( - lambda lang: bot.lang.get('ac_mode_changed_notification', lang, - ctx.user.id, ctx.user.name, - bot.lang.get(str(newmode.value), lang)), - exclude=(ctx.user_id,) + await asyncio.gather( + # reply to user + ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()), + + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('ac_mode_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(str(newmode.value), lang)), + exclude=(ctx.user_id,) + ) ) + return self.END @bot.conventer(BAT_THRESHOLDS_1) - def thresholds1_enter(self, ctx: bot.Context): + async def thresholds1_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.RECHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_1, messages=list(map(lambda n: f'{n} V', RECHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds1_input(self, ctx: bot.Context): + async def thresholds1_input(self, ctx: bot.Context): v = self._parse_voltage(ctx.text) ctx.user_data['bat_thrsh_v1'] = v - return self.invoke(self.BAT_THRESHOLDS_2, ctx) + return await self.invoke(self.BAT_THRESHOLDS_2, ctx) @bot.conventer(BAT_THRESHOLDS_2) - def thresholds2_enter(self, ctx: bot.Context): + async def thresholds2_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.REDISCHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_2, messages=list(map(lambda n: f'{n} V', REDISCHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds2_input(self, ctx: bot.Context): + async def thresholds2_input(self, ctx: bot.Context): v2 = v = self._parse_voltage(ctx.text) v1 = ctx.user_data['bat_thrsh_v1'] del ctx.user_data['bat_thrsh_v1'] response = inverter.exec('set-charge-thresholds', (v1, v2)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(AC_MAX_CHARGING_CURRENT) - def ac_max_enter(self, ctx: bot.Context): + async def ac_max_enter(self, ctx: bot.Context): buttons = self._get_allowed_ac_charge_amps() buttons = map(lambda n: f'{n} A', buttons) buttons = [list(buttons)] - return self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(AC_MAX_CHARGING_CURRENT, regex=r'^\d+ A$') - def ac_max_input(self, ctx: bot.Context): + async def ac_max_input(self, ctx: bot.Context): a = self._parse_amps(ctx.text) allowed = self._get_allowed_ac_charge_amps() if a not in allowed: raise ValueError('input is not allowed') response = inverter.exec('set-max-ac-charge-current', (0, a)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(BAT_CUT_OFF_VOLTAGE) - def cutoff_enter(self, ctx: bot.Context): - return self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, - with_back=True) + async def cutoff_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, + with_back=True) @bot.convinput(BAT_CUT_OFF_VOLTAGE, regex=r'^(\d{2}(\.\d{1})?)$') - def cutoff_input(self, ctx: bot.Context): + async def cutoff_input(self, ctx: bot.Context): v = float(ctx.text) if 40.0 <= v <= 48.0: response = inverter.exec('set-battery-cutoff-voltage', (v,)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) else: raise ValueError('invalid voltage') @@ -660,38 +678,38 @@ class ConsumptionConversation(bot.conversation): INTERVAL_BUTTONS_FLAT = list(itertools.chain.from_iterable(INTERVAL_BUTTONS)) @bot.conventer(START, message='consumption') - def start_enter(self, ctx: bot.Context): - return self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], - with_cancel=True) + async def start_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], + with_cancel=True) @bot.convinput(START, messages={ 'consumption_total': TOTAL, 'consumption_grid': GRID }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(TOTAL) - def total_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.TOTAL) + async def total_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.TOTAL) @bot.conventer(GRID) - def grid_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.GRID) + async def grid_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.GRID) - def _render_interval_btns(self, ctx: bot.Context, state): - return self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, - with_back=True) + async def _render_interval_btns(self, ctx: bot.Context, state): + return await self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, + with_back=True) @bot.convinput(TOTAL, messages=INTERVAL_BUTTONS_FLAT) - def total_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.TOTAL) + async def total_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.TOTAL) @bot.convinput(GRID, messages=INTERVAL_BUTTONS_FLAT) - def grid_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.GRID) + async def grid_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.GRID) - def _render_interval_results(self, ctx: bot.Context, state): + async def _render_interval_results(self, ctx: bot.Context, state): # if ctx.text == ctx.lang('to_select_interval'): # TODO # pass @@ -715,41 +733,43 @@ class ConsumptionConversation(bot.conversation): # [InlineKeyboardButton(ctx.lang('please_wait'), callback_data='wait')] # ]) - message = ctx.reply(ctx.lang('consumption_request_sent'), - markup=bot.IgnoreMarkup()) + message = await ctx.reply(ctx.lang('consumption_request_sent'), + markup=bot.IgnoreMarkup()) - api = WebAPIClient(timeout=60) + api = WebApiClient(timeout=60) method = 'inverter_get_consumed_energy' if state == self.TOTAL else 'inverter_get_grid_consumed_energy' try: wh = getattr(api, method)(s_from, s_to) - bot.delete_message(message.chat_id, message.message_id) - ctx.reply('%.2f Wh' % (wh,), - markup=bot.IgnoreMarkup()) + await bot.delete_message(message.chat_id, message.message_id) + await ctx.reply('%.2f Wh' % (wh,), + markup=bot.IgnoreMarkup()) return self.END except Exception as e: - bot.delete_message(message.chat_id, message.message_id) - ctx.reply_exc(e) + await asyncio.gather( + bot.delete_message(message.chat_id, message.message_id), + ctx.reply_exc(e) + ) # other # ----- @bot.handler(command='monstatus') -def monstatus_handler(ctx: bot.Context) -> None: +async def monstatus_handler(ctx: bot.Context) -> None: msg = '' st = monitor.dump_status() for k, v in st.items(): msg += k + ': ' + str(v) + '\n' - ctx.reply(msg) + await ctx.reply(msg) @bot.handler(command='monsetcur') -def monsetcur_handler(ctx: bot.Context) -> None: - ctx.reply('not implemented yet') +async def monsetcur_handler(ctx: bot.Context) -> None: + await ctx.reply('not implemented yet') @bot.callbackhandler -def button_callback(ctx: bot.Context) -> None: +async def button_callback(ctx: bot.Context) -> None: query = ctx.callback_query if query.data.startswith('flag_'): @@ -762,7 +782,7 @@ def button_callback(ctx: bot.Context) -> None: json_key = k break if not found: - query.answer(ctx.lang('flags_invalid')) + await query.answer(ctx.lang('flags_invalid')) return flags = inverter.exec('get-flags')['data'] @@ -773,32 +793,31 @@ def button_callback(ctx: bot.Context) -> None: response = inverter.exec('set-flag', (flag, target_flag_value)) # notify user - query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) + await query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) # edit message flags[json_key] = not cur_flag_value text, markup = build_flags_keyboard(flags, ctx) - query.edit_message_text(text, reply_markup=markup) + await query.edit_message_text(text, reply_markup=markup) else: - query.answer(ctx.lang('unexpected_callback_data')) + await query.answer(ctx.lang('unexpected_callback_data')) @bot.exceptionhandler -def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: +async def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: if isinstance(e, InverterError): try: err = json.loads(str(e))['message'] except json.decoder.JSONDecodeError: err = str(e) err = re.sub(r'((?:.*)?error:) (.*)', r'<b>\1</b> \2', err) - ctx.reply(err, - markup=bot.IgnoreMarkup()) + await ctx.reply(err, markup=bot.IgnoreMarkup()) return True @bot.handler(message='status') -def status_handler(ctx: bot.Context) -> None: +async def status_handler(ctx: bot.Context) -> None: gs = inverter.exec('get-status')['data'] rated = inverter.exec('get-rated')['data'] @@ -842,11 +861,11 @@ def status_handler(ctx: bot.Context) -> None: html += f'\n<b>{ctx.lang("priority")}</b>: {rated["output_source_priority"]}' # send response - ctx.reply(html) + await ctx.reply(html) @bot.handler(message='generation') -def generation_handler(ctx: bot.Context) -> None: +async def generation_handler(ctx: bot.Context) -> None: today = datetime.date.today() yday = today - datetime.timedelta(days=1) yday2 = today - datetime.timedelta(days=2) @@ -876,7 +895,7 @@ def generation_handler(ctx: bot.Context) -> None: html += f'\n<b>{ctx.lang("yday2")}:</b> %s Wh' % (gen_yday2['wh']) # send response - ctx.reply(html) + await ctx.reply(html) @bot.defaultreplymarkup @@ -920,7 +939,7 @@ class InverterStore(bot.BotDatabase): inverter.init(host=config['inverter']['ip'], port=config['inverter']['port']) bot.set_database(InverterStore()) -bot.enable_logging(BotType.INVERTER) +#bot.enable_logging(BotType.INVERTER) bot.add_conversation(SettingsConversation(enable_back=True)) bot.add_conversation(ConsumptionConversation(enable_back=True)) diff --git a/src/inverter_mqtt_util.py b/bin/inverter_mqtt_util.py index 791bf80..6003c62 100755 --- a/src/inverter_mqtt_util.py +++ b/bin/inverter_mqtt_util.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import __py_include + from argparse import ArgumentParser -from home.config import config, app_config -from home.mqtt import MqttWrapper, MqttNode +from homekit.config import config +from homekit.mqtt import MqttWrapper, MqttNode if __name__ == '__main__': @@ -17,8 +19,8 @@ if __name__ == '__main__': node = MqttNode(node_id='inverter') module_kwargs = {} if mode == 'sender': - module_kwargs['status_poll_freq'] = int(app_config['poll_freq']) - module_kwargs['generation_poll_freq'] = int(app_config['generation_poll_freq']) + module_kwargs['status_poll_freq'] = int(config.app_config['poll_freq']) + module_kwargs['generation_poll_freq'] = int(config.app_config['generation_poll_freq']) node.load_module('inverter', **module_kwargs) mqtt.add_node(node) diff --git a/src/inverterd_emulator.py b/bin/inverterd_emulator.py index 8c4d0bd..371d955 100755 --- a/src/inverterd_emulator.py +++ b/bin/inverterd_emulator.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import logging +import __py_include -from home.inverter.emulator import InverterEmulator +from homekit.inverter.emulator import InverterEmulator if __name__ == '__main__': diff --git a/bin/ipcam_capture.py b/bin/ipcam_capture.py new file mode 100755 index 0000000..226e12e --- /dev/null +++ b/bin/ipcam_capture.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import __py_include +import sys +import os +import subprocess +import asyncio +import signal + +from typing import TextIO +from argparse import ArgumentParser +from socket import gethostname +from asyncio.streams import StreamReader +from homekit.config import LinuxBoardsConfig, config as homekit_config +from homekit.camera import IpcamConfig, CaptureType +from homekit.camera.util import get_hls_directory, get_hls_channel_name, get_recordings_path + +ipcam_config = IpcamConfig() +lbc_config = LinuxBoardsConfig() +channels = (1, 2) +tasks = [] +restart_delay = 3 +lock = asyncio.Lock() +worker_type: CaptureType + + +async def read_output(stream: StreamReader, + thread_name: str, + output: TextIO): + try: + while True: + line = await stream.readline() + if not line: + break + print(f"[{thread_name}] {line.decode().strip()}", file=output) + + except asyncio.LimitOverrunError: + print(f"[{thread_name}] Output limit exceeded.", file=output) + + except Exception as e: + print(f"[{thread_name}] Error occurred while reading output: {e}", file=sys.stderr) + + +async def run_ffmpeg(cam: int, channel: int): + prefix = get_hls_channel_name(cam, channel) + + if homekit_config.app_config.logging_is_verbose(): + debug_args = ['-v', '-info'] + else: + debug_args = ['-nostats', '-loglevel', 'error'] + + # protocol = 'tcp' if ipcam_config.should_use_tcp_for_rtsp(cam) else 'udp' + protocol = 'tcp' + user, pw = ipcam_config.get_rtsp_creds() + ip = ipcam_config.get_camera_ip(cam) + path = ipcam_config.get_camera_type(cam).get_channel_url(channel) + ext = ipcam_config.get_camera_container(cam) + ffmpeg_command = ['ffmpeg', *debug_args, + '-rtsp_transport', protocol, + '-i', f'rtsp://{user}:{pw}@{ip}:554{path}', + '-c', 'copy',] + + if worker_type == CaptureType.HLS: + ffmpeg_command.extend(['-bufsize', '1835k', + '-pix_fmt', 'yuv420p', + '-flags', '-global_header', + '-hls_time', '2', + '-hls_list_size', '3', + '-hls_flags', 'delete_segments', + os.path.join(get_hls_directory(cam, channel), 'live.m3u8')]) + + elif worker_type == CaptureType.RECORD: + ffmpeg_command.extend(['-f', 'segment', + '-strftime', '1', + '-segment_time', '00:10:00', + '-segment_atclocktime', '1', + os.path.join(get_recordings_path(cam), f'record_%Y-%m-%d-%H.%M.%S.{ext.value}')]) + + else: + raise ValueError(f'invalid worker type: {worker_type}') + + while True: + try: + process = await asyncio.create_subprocess_exec( + *ffmpeg_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + stdout_task = asyncio.create_task(read_output(process.stdout, prefix, sys.stdout)) + stderr_task = asyncio.create_task(read_output(process.stderr, prefix, sys.stderr)) + + await asyncio.gather(stdout_task, stderr_task) + + # check the return code of the process + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, ffmpeg_command) + + except (FileNotFoundError, PermissionError, subprocess.CalledProcessError) as e: + # an error occurred, print the error message + error_message = f"Error occurred in {prefix}: {e}" + print(error_message, file=sys.stderr) + + # sleep for 5 seconds before restarting the process + await asyncio.sleep(restart_delay) + + +async def run(): + kwargs = {} + if worker_type == CaptureType.RECORD: + kwargs['filter_by_server'] = gethostname() + for cam in ipcam_config.get_all_cam_names(**kwargs): + for channel in channels: + task = asyncio.create_task(run_ffmpeg(cam, channel)) + tasks.append(task) + + try: + await asyncio.gather(*tasks) + except KeyboardInterrupt: + print('KeyboardInterrupt: stopping processes...', file=sys.stderr) + for task in tasks: + task.cancel() + + # wait for subprocesses to terminate + await asyncio.gather(*tasks, return_exceptions=True) + + # send termination signal to all subprocesses + for task in tasks: + process = task.get_stack() + if process: + process.send_signal(signal.SIGTERM) + + +if __name__ == '__main__': + capture_types = [t.value for t in CaptureType] + parser = ArgumentParser() + parser.add_argument('type', type=str, metavar='CAPTURE_TYPE', choices=tuple(capture_types), + help='capture type (variants: '+', '.join(capture_types)+')') + + arg = homekit_config.load_app(no_config=True, parser=parser) + worker_type = CaptureType(arg['type']) + + asyncio.run(run()) diff --git a/tools/ipcam_motion_worker.sh b/bin/ipcam_motion_worker.sh index c5f711d..603a407 100755 --- a/tools/ipcam_motion_worker.sh +++ b/bin/ipcam_motion_worker.sh @@ -5,7 +5,7 @@ set -e DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &>/dev/null && pwd )" PROGNAME="$0" -. "$DIR/lib.bash" +. "$DIR/../include/bash/include.bash" curl_opts="-s --connect-timeout 10 --retry 5 --max-time 180 --retry-delay 0 --retry-max-time 180" allow_multiple= diff --git a/bin/ipcam_ntp_util.py b/bin/ipcam_ntp_util.py new file mode 100755 index 0000000..98639bd --- /dev/null +++ b/bin/ipcam_ntp_util.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +import __py_include +import requests +import hashlib +import xml.etree.ElementTree as ET + +from time import time +from argparse import ArgumentParser, ArgumentError +from homekit.util import validate_ipv4, validate_ipv4_or_hostname +from homekit.camera import IpcamConfig + + +def xml_to_dict(xml_data: str) -> dict: + # Parse the XML data + root = ET.fromstring(xml_data) + + # Function to remove namespace from the tag name + def remove_namespace(tag): + return tag.split('}')[-1] # Splits on '}' and returns the last part, the actual tag name without namespace + + # Function to recursively convert XML elements to a dictionary + def elem_to_dict(elem): + tag = remove_namespace(elem.tag) + elem_dict = {tag: {}} + + # If the element has attributes, add them to the dictionary + elem_dict[tag].update({'@' + remove_namespace(k): v for k, v in elem.attrib.items()}) + + # Handle the element's text content, if present and not just whitespace + text = elem.text.strip() if elem.text and elem.text.strip() else None + if text: + elem_dict[tag]['#text'] = text + + # Process child elements + for child in elem: + child_dict = elem_to_dict(child) + child_tag = remove_namespace(child.tag) + if child_tag not in elem_dict[tag]: + elem_dict[tag][child_tag] = [] + elem_dict[tag][child_tag].append(child_dict[child_tag]) + + # Simplify structure if there's only text or no children and no attributes + if len(elem_dict[tag]) == 1 and '#text' in elem_dict[tag]: + return {tag: elem_dict[tag]['#text']} + elif not elem_dict[tag]: + return {tag: ''} + + return elem_dict + + # Convert the root element to dictionary + return elem_to_dict(root) + + +def sha256_hex(input_string: str) -> str: + return hashlib.sha256(input_string.encode()).hexdigest() + + +class ResponseError(RuntimeError): + pass + + +class AuthError(ResponseError): + pass + + +class HikvisionISAPIClient: + def __init__(self, host): + self.host = host + self.cookies = {} + + def auth(self, username: str, password: str): + r = requests.get(self.isapi_uri('Security/sessionLogin/capabilities'), + {'username': username}, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + }) + r.raise_for_status() + caps = xml_to_dict(r.text)['SessionLoginCap'] + is_irreversible = caps['isIrreversible'][0].lower() == 'true' + + # https://github.com/JakeVincet/nvt/blob/master/2018/hikvision/gb_hikvision_ip_camera_default_credentials.nasl + # also look into webAuth.js and utils.js + + if 'salt' in caps and is_irreversible: + p = sha256_hex(username + caps['salt'][0] + password) + p = sha256_hex(p + caps['challenge'][0]) + for i in range(int(caps['iterations'][0])-2): + p = sha256_hex(p) + else: + p = sha256_hex(password) + caps['challenge'][0] + for i in range(int(caps['iterations'][0])-1): + p = sha256_hex(p) + + data = '<SessionLogin>' + data += f'<userName>{username}</userName>' + data += f'<password>{p}</password>' + data += f'<sessionID>{caps["sessionID"][0]}</sessionID>' + data += '<isSessionIDValidLongTerm>false</isSessionIDValidLongTerm>' + data += f'<sessionIDVersion>{caps["sessionIDVersion"][0]}</sessionIDVersion>' + data += '</SessionLogin>' + + r = requests.post(self.isapi_uri(f'Security/sessionLogin?timeStamp={int(time())}'), data=data, headers={ + 'Accept-Encoding': 'gzip, deflate', + 'If-Modified-Since': '0', + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }) + r.raise_for_status() + resp = xml_to_dict(r.text)['SessionLogin'] + status_value = int(resp['statusValue'][0]) + status_string = resp['statusString'][0] + if status_value != 200: + raise AuthError(f'{status_value}: {status_string}') + + self.cookies = r.cookies.get_dict() + + def get_ntp_server(self) -> str: + r = requests.get(self.isapi_uri('System/time/ntpServers/capabilities'), cookies=self.cookies) + r.raise_for_status() + ntp_server = xml_to_dict(r.text)['NTPServerList']['NTPServer'][0] + + if ntp_server['addressingFormatType'][0]['#text'] == 'hostname': + ntp_host = ntp_server['hostName'][0] + else: + ntp_host = ntp_server['ipAddress'][0] + + return ntp_host + + def set_timezone(self): + data = '<?xml version="1.0" encoding="UTF-8"?>' + data += '<Time><timeMode>NTP</timeMode><timeZone>CST-3:00:00</timeZone></Time>' + + r = requests.put(self.isapi_uri('System/time'), cookies=self.cookies, data=data, headers={ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) + self.isapi_check_put_response(r) + + def set_ntp_server(self, ntp_host: str, ntp_port: int = 123): + format = 'ipaddress' if validate_ipv4(ntp_host) else 'hostname' + + data = '<?xml version="1.0" encoding="UTF-8"?>' + data += f'<NTPServer><id>1</id><addressingFormatType>{format}</addressingFormatType><ipAddress>{ntp_host}</ipAddress><portNo>{ntp_port}</portNo><synchronizeInterval>1440</synchronizeInterval></NTPServer>' + + r = requests.put(self.isapi_uri('System/time/ntpServers/1'), + data=data, + cookies=self.cookies, + headers={ + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }) + self.isapi_check_put_response(r) + + def isapi_uri(self, path: str) -> str: + return f'http://{self.host}/ISAPI/{path}' + + def isapi_check_put_response(self, r): + r.raise_for_status() + resp = xml_to_dict(r.text)['ResponseStatus'] + + status_code = int(resp['statusCode'][0]) + status_string = resp['statusString'][0] + + if status_code != 1 or status_string.lower() != 'ok': + raise ResponseError('response status looks bad') + + +def main(): + parser = ArgumentParser() + parser.add_argument('--host', type=str, required=True) + parser.add_argument('--get-ntp-server', action='store_true') + parser.add_argument('--set-ntp-server', type=str) + parser.add_argument('--username', type=str) + parser.add_argument('--password', type=str) + args = parser.parse_args() + + if not args.get_ntp_server and not args.set_ntp_server: + raise ArgumentError(None, 'either --get-ntp-server or --set-ntp-server is required') + + ipcam_config = IpcamConfig() + login = args.username if args.username else ipcam_config['web_creds']['login'] + password = args.password if args.password else ipcam_config['web_creds']['password'] + + client = HikvisionISAPIClient(args.host) + client.auth(args.username, args.password) + + if args.get_ntp_server: + print(client.get_ntp_server()) + return + + if not args.set_ntp_server: + raise ArgumentError(None, '--set-ntp-server is required') + + if not validate_ipv4_or_hostname(args.set_ntp_server): + raise ArgumentError(None, 'input ntp server is neither ip address nor a valid hostname') + + client.set_ntp_server(args.set_ntp_server) + + +if __name__ == '__main__': + main()
\ No newline at end of file diff --git a/src/ipcam_server.py b/bin/ipcam_server.py index a54cd35..71d5ea1 100755 --- a/src/ipcam_server.py +++ b/bin/ipcam_server.py @@ -1,58 +1,52 @@ #!/usr/bin/env python3 import logging import os -import re import asyncio import time import shutil -import home.telegram.aio as telegram +import __py_include +import homekit.telegram.aio as telegram + +from socket import gethostname +from argparse import ArgumentParser from apscheduler.schedulers.asyncio import AsyncIOScheduler from asyncio import Lock -from home.config import config -from home import http -from home.database.sqlite import SQLiteBase -from home.camera import util as camutil +from homekit.config import config as homekit_config, LinuxBoardsConfig +from homekit.util import Addr +from homekit import http +from homekit.database.sqlite import SQLiteBase +from homekit.camera import util as camutil, IpcamConfig +from homekit.camera.types import ( + TimeFilterType, + TelegramLinkType, + VideoContainerType +) +from homekit.camera.util import ( + get_recordings_path, + get_motion_path, + is_valid_recording_name, + datetime_from_filename +) -from enum import Enum from typing import Optional, Union, List, Tuple from datetime import datetime, timedelta from functools import cmp_to_key -class TimeFilterType(Enum): - FIX = 'fix' - MOTION = 'motion' - MOTION_START = 'motion_start' - - -class TelegramLinkType(Enum): - FRAGMENT = 'fragment' - ORIGINAL_FILE = 'original_file' - - -def valid_recording_name(filename: str) -> bool: - return filename.startswith('record_') and filename.endswith('.mp4') - - -def filename_to_datetime(filename: str) -> datetime: - filename = os.path.basename(filename).replace('record_', '').replace('.mp4', '') - return datetime.strptime(filename, datetime_format) - - -def get_all_cams() -> list: - return [cam for cam in config['camera'].keys()] +ipcam_config = IpcamConfig() +lbc_config = LinuxBoardsConfig() # ipcam database # -------------- -class IPCamServerDatabase(SQLiteBase): +class IpcamServerDatabase(SQLiteBase): SCHEMA = 4 - def __init__(self): - super().__init__() + def __init__(self, path=None): + super().__init__(path=path) def schema_init(self, version: int) -> None: cursor = self.cursor() @@ -64,7 +58,7 @@ class IPCamServerDatabase(SQLiteBase): fix_time INTEGER NOT NULL, motion_time INTEGER NOT NULL )""") - for cam in config['camera'].keys(): + for cam in ipcam_config.get_all_cam_names_for_this_server(): self.add_camera(cam) if version < 2: @@ -132,7 +126,7 @@ class IPCamServerDatabase(SQLiteBase): # ipcam web api # ------------- -class IPCamWebServer(http.HTTPServer): +class IpcamWebServer(http.HTTPServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -143,16 +137,16 @@ class IPCamWebServer(http.HTTPServer): self.get('/api/timestamp/{name}/{type}', self.get_timestamp) self.get('/api/timestamp/all', self.get_all_timestamps) - self.post('/api/debug/migrate-mtimes', self.debug_migrate_mtimes) self.post('/api/debug/fix', self.debug_fix) self.post('/api/debug/cleanup', self.debug_cleanup) + self.post('/api/timestamp/{name}/{type}', self.set_timestamp) self.post('/api/motion/done/{name}', self.submit_motion) self.post('/api/motion/fail/{name}', self.submit_motion_failure) - self.get('/api/motion/params/{name}', self.get_motion_params) - self.get('/api/motion/params/{name}/roi', self.get_motion_roi_params) + # self.get('/api/motion/params/{name}', self.get_motion_params) + # self.get('/api/motion/params/{name}/roi', self.get_motion_roi_params) self.queue_lock = Lock() @@ -170,7 +164,7 @@ class IPCamWebServer(http.HTTPServer): files = get_recordings_files(camera, filter, limit) if files: - time = filename_to_datetime(files[len(files)-1]['name']) + time = datetime_from_filename(files[len(files)-1]['name']) db.set_timestamp(camera, TimeFilterType.MOTION_START, time) return self.ok({'files': files}) @@ -185,7 +179,7 @@ class IPCamWebServer(http.HTTPServer): if files: times_by_cam = {} for file in files: - time = filename_to_datetime(file['name']) + time = datetime_from_filename(file['name']) if file['cam'] not in times_by_cam or times_by_cam[file['cam']] < time: times_by_cam[file['cam']] = time for cam, time in times_by_cam.items(): @@ -197,14 +191,14 @@ class IPCamWebServer(http.HTTPServer): cam = int(req.match_info['name']) file = req.match_info['file'] - fullpath = os.path.join(config['camera'][cam]['recordings_path'], file) + fullpath = os.path.join(get_recordings_path(cam), file) if not os.path.isfile(fullpath): raise ValueError(f'file "{fullpath}" does not exists') return http.FileResponse(fullpath) async def camlist(self, req: http.Request): - return self.ok(config['camera']) + return self.ok(ipcam_config.get_all_cam_names_for_this_server()) async def submit_motion(self, req: http.Request): data = await req.post() @@ -213,7 +207,7 @@ class IPCamWebServer(http.HTTPServer): timecodes = data['timecodes'] filename = data['filename'] - time = filename_to_datetime(filename) + time = datetime_from_filename(filename) try: if timecodes != '': @@ -236,27 +230,10 @@ class IPCamWebServer(http.HTTPServer): message = data['message'] db.add_motion_failure(camera, filename, message) - db.set_timestamp(camera, TimeFilterType.MOTION, filename_to_datetime(filename)) + db.set_timestamp(camera, TimeFilterType.MOTION, datetime_from_filename(filename)) return self.ok() - async def debug_migrate_mtimes(self, req: http.Request): - written = {} - for cam in config['camera'].keys(): - confdir = os.path.join(os.getenv('HOME'), '.config', f'video-util-{cam}') - for time_type in TimeFilterType: - txt_file = os.path.join(confdir, f'{time_type.value}_mtime') - if os.path.isfile(txt_file): - with open(txt_file, 'r') as fd: - data = fd.read() - db.set_timestamp(cam, time_type, int(data.strip())) - - if cam not in written: - written[cam] = [] - written[cam].append(time_type) - - return self.ok({'written': written}) - async def debug_fix(self, req: http.Request): asyncio.ensure_future(fix_job()) return self.ok() @@ -277,26 +254,26 @@ class IPCamWebServer(http.HTTPServer): async def get_all_timestamps(self, req: http.Request): return self.ok(db.get_all_timestamps()) - async def get_motion_params(self, req: http.Request): - data = config['motion_params'][int(req.match_info['name'])] - lines = [ - f'threshold={data["threshold"]}', - f'min_event_length=3s', - f'frame_skip=2', - f'downscale_factor=3', - ] - return self.plain('\n'.join(lines)+'\n') - - async def get_motion_roi_params(self, req: http.Request): - data = config['motion_params'][int(req.match_info['name'])] - return self.plain('\n'.join(data['roi'])+'\n') + # async def get_motion_params(self, req: http.Request): + # data = config['motion_params'][int(req.match_info['name'])] + # lines = [ + # f'threshold={data["threshold"]}', + # f'min_event_length=3s', + # f'frame_skip=2', + # f'downscale_factor=3', + # ] + # return self.plain('\n'.join(lines)+'\n') + # + # async def get_motion_roi_params(self, req: http.Request): + # data = config['motion_params'][int(req.match_info['name'])] + # return self.plain('\n'.join(data['roi'])+'\n') @staticmethod def _getset_timestamp_params(req: http.Request, need_time=False): values = [] cam = int(req.match_info['name']) - assert cam in config['camera'], 'invalid camera' + assert cam in ipcam_config.get_all_cam_names_for_this_server(), 'invalid camera' values.append(cam) values.append(TimeFilterType(req.match_info['type'])) @@ -304,7 +281,7 @@ class IPCamWebServer(http.HTTPServer): if need_time: time = req.query['time'] if time.startswith('record_'): - time = filename_to_datetime(time) + time = datetime_from_filename(time) elif time.isnumeric(): time = int(time) else: @@ -317,32 +294,24 @@ class IPCamWebServer(http.HTTPServer): # other global stuff # ------------------ -def open_database(): +def open_database(database_path: str): global db - db = IPCamServerDatabase() + db = IpcamServerDatabase(database_path) # update cams list in database, if needed - cams = db.get_all_timestamps().keys() - for cam in config['camera']: - if cam not in cams: + stored_cams = db.get_all_timestamps().keys() + for cam in ipcam_config.get_all_cam_names_for_this_server(): + if cam not in stored_cams: db.add_camera(cam) -def get_recordings_path(cam: int) -> str: - return config['camera'][cam]['recordings_path'] - - -def get_motion_path(cam: int) -> str: - return config['camera'][cam]['motion_path'] - - def get_recordings_files(cam: Optional[int] = None, time_filter_type: Optional[TimeFilterType] = None, limit=0) -> List[dict]: from_time = 0 to_time = int(time.time()) - cams = [cam] if cam is not None else get_all_cams() + cams = [cam] if cam is not None else ipcam_config.get_all_cam_names_for_this_server() files = [] for cam in cams: if time_filter_type: @@ -359,7 +328,7 @@ def get_recordings_files(cam: Optional[int] = None, 'name': file, 'size': os.path.getsize(os.path.join(recdir, file))} for file in os.listdir(recdir) - if valid_recording_name(file) and from_time < filename_to_datetime(file) <= to_time] + if is_valid_recording_name(file) and from_time < datetime_from_filename(file) <= to_time] cam_files.sort(key=lambda file: file['name']) if cam_files: @@ -379,7 +348,7 @@ def get_recordings_files(cam: Optional[int] = None, async def process_fragments(camera: int, filename: str, fragments: List[Tuple[int, int]]) -> None: - time = filename_to_datetime(filename) + time = datetime_from_filename(filename) rec_dir = get_recordings_path(camera) motion_dir = get_motion_path(camera) @@ -389,8 +358,8 @@ async def process_fragments(camera: int, for fragment in fragments: start, end = fragment - start -= config['motion']['padding'] - end += config['motion']['padding'] + start -= ipcam_config['motion_padding'] + end += ipcam_config['motion_padding'] if start < 0: start = 0 @@ -405,14 +374,14 @@ async def process_fragments(camera: int, start_pos=start, duration=duration) - if fragments and 'telegram' in config['motion'] and config['motion']['telegram']: + if fragments and ipcam_config['motion_telegram']: asyncio.ensure_future(motion_notify_tg(camera, filename, fragments)) async def motion_notify_tg(camera: int, filename: str, fragments: List[Tuple[int, int]]): - dt_file = filename_to_datetime(filename) + dt_file = datetime_from_filename(filename) fmt = '%H:%M:%S' text = f'Camera: <b>{camera}</b>\n' @@ -420,8 +389,8 @@ async def motion_notify_tg(camera: int, text += _tg_links(TelegramLinkType.ORIGINAL_FILE, camera, filename) for start, end in fragments: - start -= config['motion']['padding'] - end += config['motion']['padding'] + start -= ipcam_config['motion_padding'] + end += ipcam_config['motion_padding'] if start < 0: start = 0 @@ -443,7 +412,7 @@ def _tg_links(link_type: TelegramLinkType, camera: int, file: str) -> str: links = [] - for link_name, link_template in config['telegram'][f'{link_type.value}_url_templates']: + for link_name, link_template in ipcam_config[f'{link_type.value}_url_templates']: link = link_template.replace('{camera}', str(camera)).replace('{file}', file) links.append(f'<a href="{link}">{link_name}</a>') return ' '.join(links) @@ -459,7 +428,7 @@ async def fix_job() -> None: try: fix_job_running = True - for cam in config['camera'].keys(): + for cam in ipcam_config.get_all_cam_names_for_this_server(): files = get_recordings_files(cam, TimeFilterType.FIX) if not files: logger.debug(f'fix_job: no files for camera {cam}') @@ -470,7 +439,7 @@ async def fix_job() -> None: for file in files: fullpath = os.path.join(get_recordings_path(cam), file['name']) await camutil.ffmpeg_recreate(fullpath) - timestamp = filename_to_datetime(file['name']) + timestamp = datetime_from_filename(file['name']) if timestamp: db.set_timestamp(cam, TimeFilterType.FIX, timestamp) @@ -479,21 +448,9 @@ async def fix_job() -> None: async def cleanup_job() -> None: - def fn2dt(name: str) -> datetime: - name = os.path.basename(name) - - if name.startswith('record_'): - return datetime.strptime(re.match(r'record_(.*?)\.mp4', name).group(1), datetime_format) - - m = re.match(rf'({datetime_format_re})__{datetime_format_re}\.mp4', name) - if m: - return datetime.strptime(m.group(1), datetime_format) - - raise ValueError(f'unrecognized filename format: {name}') - def compare(i1: str, i2: str) -> int: - dt1 = fn2dt(i1) - dt2 = fn2dt(i2) + dt1 = datetime_from_filename(i1) + dt2 = datetime_from_filename(i2) if dt1 < dt2: return -1 @@ -513,18 +470,19 @@ async def cleanup_job() -> None: cleanup_job_running = True gb = float(1 << 30) - for storage in config['storages']: + disk_number = 0 + for storage in lbc_config.get_board_disks(gethostname()): + disk_number += 1 if os.path.exists(storage['mountpoint']): total, used, free = shutil.disk_usage(storage['mountpoint']) free_gb = free // gb - if free_gb < config['cleanup_min_gb']: - # print(f"{storage['mountpoint']}: free={free}, free_gb={free_gb}") + if free_gb < ipcam_config['cleanup_min_gb']: cleaned = 0 files = [] - for cam in storage['cams']: - for _dir in (config['camera'][cam]['recordings_path'], config['camera'][cam]['motion_path']): + for cam in ipcam_config.get_all_cam_names_for_this_server(filter_by_disk=disk_number): + for _dir in (get_recordings_path(cam), get_motion_path(cam)): files += list(map(lambda file: os.path.join(_dir, file), os.listdir(_dir))) - files = list(filter(lambda path: os.path.isfile(path) and path.endswith('.mp4'), files)) + files = list(filter(lambda path: os.path.isfile(path) and path.endswith(tuple([f'.{t.value}' for t in VideoContainerType])), files)) files.sort(key=cmp_to_key(compare)) for file in files: @@ -534,7 +492,7 @@ async def cleanup_job() -> None: cleaned += size except OSError as e: logger.exception(e) - if (free + cleaned) // gb >= config['cleanup_min_gb']: + if (free + cleaned) // gb >= ipcam_config['cleanup_min_gb']: break else: logger.error(f"cleanup_job: {storage['mountpoint']} not found") @@ -547,8 +505,8 @@ cleanup_job_running = False datetime_format = '%Y-%m-%d-%H.%M.%S' datetime_format_re = r'\d{4}-\d{2}-\d{2}-\d{2}\.\d{2}.\d{2}' -db: Optional[IPCamServerDatabase] = None -server: Optional[IPCamWebServer] = None +db: Optional[IpcamServerDatabase] = None +server: Optional[IpcamWebServer] = None logger = logging.getLogger(__name__) @@ -556,18 +514,25 @@ logger = logging.getLogger(__name__) # -------------------- if __name__ == '__main__': - config.load_app('ipcam_server') + parser = ArgumentParser() + parser.add_argument('--listen', type=str, required=True) + parser.add_argument('--database-path', type=str, required=True) + arg = homekit_config.load_app(no_config=True, parser=parser) - open_database() + open_database(arg.database_path) loop = asyncio.get_event_loop() try: scheduler = AsyncIOScheduler(event_loop=loop) - if config['fix_enabled']: - scheduler.add_job(fix_job, 'interval', seconds=config['fix_interval'], misfire_grace_time=None) - - scheduler.add_job(cleanup_job, 'interval', seconds=config['cleanup_interval'], misfire_grace_time=None) + if ipcam_config['fix_enabled']: + scheduler.add_job(fix_job, 'interval', + seconds=ipcam_config['fix_interval'], + misfire_grace_time=None) + + scheduler.add_job(cleanup_job, 'interval', + seconds=ipcam_config['cleanup_interval'], + misfire_grace_time=None) scheduler.start() except KeyError: pass @@ -575,5 +540,5 @@ if __name__ == '__main__': asyncio.ensure_future(fix_job()) asyncio.ensure_future(cleanup_job()) - server = IPCamWebServer(config.get_addr('server.listen')) + server = IpcamWebServer(Addr.fromstring(arg.listen)) server.run() diff --git a/bin/lugovaya_pump_mqtt_bot.py b/bin/lugovaya_pump_mqtt_bot.py new file mode 100755 index 0000000..85402d1 --- /dev/null +++ b/bin/lugovaya_pump_mqtt_bot.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import datetime +import __py_include + +from enum import Enum +from typing import Optional +from telegram import ReplyKeyboardMarkup, User + +from homekit.config import config, AppConfigUnit +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig +from homekit.telegram._botutil import user_any_name +from homekit.mqtt import MqttNode, MqttPayload, MqttNodesConfig, MqttWrapper +from homekit.mqtt.module.relay import MqttRelayState, MqttRelayModule +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload + + +class LugovayaPumpMqttBotConfig(TelegramBotConfig, AppConfigUnit): + NAME = 'lugovaya_pump_mqtt_bot' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + **TelegramBotConfig.schema(), + 'relay_node_id': { + 'type': 'string', + 'required': True + }, + } + + @staticmethod + def custom_validator(data): + relay_node_names = MqttNodesConfig().get_nodes(filters=('relay',), only_names=True) + if data['relay_node_id'] not in relay_node_names: + raise ValueError('unknown relay node "%s"' % (data['relay_node_id'],)) + + +config.load_app(LugovayaPumpMqttBotConfig) + +bot.initialize() +bot.lang.ru( + start_message="Выберите команду на клавиатуре", + start_message_no_access="Доступ запрещён. Вы можете отправить заявку на получение доступа.", + unknown_command="Неизвестная команда", + send_access_request="Отправить заявку", + management="Админка", + + enable="Включить", + enabled="Включен ✅", + + disable="Выключить", + disabled="Выключен ❌", + + status="Статус", + status_updated=' (обновлено %s)', + + done="Готово 👌", + user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.', + user_action_on="включил", + user_action_off="выключил", + date_yday="вчера", + date_yyday="позавчера", + date_at="в" +) +bot.lang.en( + start_message="Select command on the keyboard", + start_message_no_access="You have no access.", + unknown_command="Unknown command", + send_access_request="Send request", + management="Admin options", + + enable="Turn ON", + enable_silently="Turn ON silently", + enabled="Turned ON ✅", + + disable="Turn OFF", + disable_silently="Turn OFF silently", + disabled="Turned OFF ❌", + + status="Status", + status_updated=' (updated %s)', + + done="Done 👌", + user_action_notification='User <a href="tg://user?id=%d">%s</a> turned the pump <b>%s</b>.', + user_action_on="ON", + user_action_off="OFF", + + date_yday="yesterday", + date_yyday="the day before yesterday", + date_at="at" +) + + +mqtt: MqttWrapper +relay_state = MqttRelayState() +relay_module: MqttRelayModule + + +class UserAction(Enum): + ON = 'on' + OFF = 'off' + + +# def on_mqtt_message(home_id, message: MqttPayload): +# if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): +# kwargs = dict(rssi=message.rssi, enabled=message.flags.state) +# if isinstance(message, InitialDiagnosticsPayload): +# kwargs['fw_version'] = message.fw_version +# relay_state.update(**kwargs) + + +async def notify(user: User, action: UserAction) -> None: + def text_getter(lang: str): + action_name = bot.lang.get(f'user_action_{action.value}', lang) + user_name = user_any_name(user) + return 'ℹ ' + bot.lang.get('user_action_notification', lang, + user.id, user_name, action_name) + + await bot.notify_all(text_getter, exclude=(user.id,)) + + +@bot.handler(message='enable') +async def enable_handler(ctx: bot.Context) -> None: + relay_module.switchpower(True) + await ctx.reply(ctx.lang('done')) + await notify(ctx.user, UserAction.ON) + + +@bot.handler(message='disable') +async def disable_handler(ctx: bot.Context) -> None: + relay_module.switchpower(False) + await ctx.reply(ctx.lang('done')) + await notify(ctx.user, UserAction.OFF) + + +@bot.handler(message='status') +async def status(ctx: bot.Context) -> None: + label = ctx.lang('enabled') if relay_state.enabled else ctx.lang('disabled') + if relay_state.ever_updated: + date_label = '' + today = datetime.date.today() + if today != relay_state.update_time.date(): + yday = today - datetime.timedelta(days=1) + yyday = today - datetime.timedelta(days=2) + if yday == relay_state.update_time.date(): + date_label = ctx.lang('date_yday') + elif yyday == relay_state.update_time.date(): + date_label = ctx.lang('date_yyday') + else: + date_label = relay_state.update_time.strftime('%d.%m.%Y') + date_label += ' ' + date_label += ctx.lang('date_at') + ' ' + date_label += relay_state.update_time.strftime('%H:%M') + label += ctx.lang('status_updated', date_label) + await ctx.reply(label) + + +async def start(ctx: bot.Context) -> None: + if ctx.user_id in config['bot']['users']: + await ctx.reply(ctx.lang('start_message')) + else: + buttons = [ + [ctx.lang('send_access_request')] + ] + await ctx.reply(ctx.lang('start_message_no_access'), + markup=ReplyKeyboardMarkup(buttons, one_time_keyboard=False)) + + +@bot.exceptionhandler +def exception_handler(e: Exception, ctx: bot.Context) -> bool: + return False + + +@bot.defaultreplymarkup +def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: + buttons = [ + [ + ctx.lang('enable'), + ctx.lang('disable') + ], + # [ctx.lang('status')] + ] + # if ctx.user_id in config['bot']['admin_users']: + # buttons.append([ctx.lang('management')]) + return ReplyKeyboardMarkup(buttons, one_time_keyboard=False) + + +node_data = MqttNodesConfig().get_node(config.app_config['relay_node_id']) + +mqtt = MqttWrapper(client_id='lugovaya_pump_mqtt_bot') +mqtt_node = MqttNode(node_id=config.app_config['relay_node_id'], + node_secret=node_data['password']) +module_kwargs = {} +try: + if node_data['relay']['legacy_topics']: + module_kwargs['legacy_topics'] = True +except KeyError: + pass +relay_module = mqtt_node.load_module('relay', **module_kwargs) +# mqtt_node.add_payload_callback(on_mqtt_message) +mqtt.add_node(mqtt_node) + +mqtt.connect_and_loop(loop_forever=False) + +bot.run(start_handler=start) + +mqtt.disconnect() diff --git a/src/mqtt_node_util.py b/bin/mqtt_node_util.py index e2ec838..639d4b9 100755 --- a/src/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -1,17 +1,43 @@ #!/usr/bin/env python3 import os.path +import __py_include from time import sleep from typing import Optional from argparse import ArgumentParser, ArgumentError -from home.config import config -from home.mqtt import MqttNode, MqttWrapper, get_mqtt_modules -from home.mqtt import MqttNodesConfig +from homekit.config import config +from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules, MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayModule +from homekit.mqtt.module.ota import MqttOtaModule mqtt_node: Optional[MqttNode] = None mqtt: Optional[MqttWrapper] = None +relay_module: Optional[MqttOtaModule] = None +relay_val = None + +ota_module: Optional[MqttRelayModule] = None +ota_val = False + +no_wait = False +stop_loop = False + + +def on_mqtt_connect(): + global stop_loop + + if relay_module: + relay_module.switchpower(relay_val == 1) + + if ota_val: + if not os.path.exists(arg.push_ota): + raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') + ota_module.push_ota(arg.push_ota, 1) + + if no_wait: + stop_loop = True + if __name__ == '__main__': nodes_config = MqttNodesConfig() @@ -23,16 +49,22 @@ if __name__ == '__main__': parser.add_argument('--switch-relay', choices=[0, 1], type=int, help='send relay state') parser.add_argument('--push-ota', type=str, metavar='OTA_FILENAME', - help='push OTA, receives path to firmware.bin') + help='push OTA, receives path to firmware.bin (not .elf!)') + parser.add_argument('--no-wait', action='store_true', + help='execute command and exit') config.load_app(parser=parser, no_config=True) arg = parser.parse_args() + if arg.no_wait: + no_wait = True + if arg.switch_relay is not None and 'relay' not in arg.modules: raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules') mqtt = MqttWrapper(randomize_client_id=True, client_id='mqtt_node_util') + mqtt.add_connect_callback(on_mqtt_connect) mqtt_node = MqttNode(node_id=arg.node_id, node_secret=nodes_config.get_node(arg.node_id)['password']) @@ -40,25 +72,29 @@ if __name__ == '__main__': # must-have modules ota_module = mqtt_node.load_module('ota') + ota_val = arg.push_ota + mqtt_node.load_module('diagnostics') if arg.modules: for m in arg.modules: - module_instance = mqtt_node.load_module(m) + kwargs = {} + if m == 'relay' and MqttNodesConfig().node_uses_legacy_relay_power_payload(arg.node_id): + kwargs['legacy_topics'] = True + if m == 'temphum' and MqttNodesConfig().node_uses_legacy_temphum_data_payload(arg.node_id): + kwargs['legacy_payload'] = True + module_instance = mqtt_node.load_module(m, **kwargs) if m == 'relay' and arg.switch_relay is not None: - module_instance.switchpower(arg.switch_relay == 1) + relay_module = module_instance + relay_val = arg.switch_relay - mqtt.configure_tls() try: mqtt.connect_and_loop(loop_forever=False) - - if arg.push_ota: - if not os.path.exists(arg.push_ota): - raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') - ota_module.push_ota(arg.push_ota, 1) - - while True: + while not stop_loop: sleep(0.1) except KeyboardInterrupt: + pass + + finally: mqtt.disconnect() diff --git a/bin/openwrt_log_analyzer.py b/bin/openwrt_log_analyzer.py new file mode 100755 index 0000000..5b14a2f --- /dev/null +++ b/bin/openwrt_log_analyzer.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import __py_include +import homekit.telegram as telegram + +from homekit.telegram.config import TelegramChatsConfig +from homekit.util import validate_mac_address +from typing import Optional +from homekit.config import config, AppConfigUnit +from homekit.database import BotsDatabase, SimpleState + + +class OpenwrtLogAnalyzerConfig(AppConfigUnit): + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'database_name': {'type': 'string', 'required': True}, + 'devices': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': { + 'type': 'string', + 'check_with': validate_mac_address + } + }, + 'limit': {'type': 'integer'}, + 'telegram_chat': {'type': 'string'}, + 'aps': { + 'type': 'list', + 'schema': {'type': 'integer'} + } + } + + @staticmethod + def custom_validator(data): + chats = TelegramChatsConfig() + if data['telegram_chat'] not in chats: + return ValueError(f'unknown telegram chat {data["telegram_chat"]}') + + +def main(mac: str, + title: str, + ap: int) -> int: + db = BotsDatabase() + + data = db.get_openwrt_logs(filter_text=mac, + min_id=state['last_id'], + access_point=ap, + limit=config['openwrt_log_analyzer']['limit']) + if not data: + return 0 + + max_id = 0 + for log in data: + if log.id > max_id: + max_id = log.id + + text = '\n'.join(map(lambda s: str(s), data)) + telegram.send_message(f'<b>{title} (AP #{ap})</b>\n\n' + text, config.app_config['telegram_chat']) + + return max_id + + +if __name__ == '__main__': + config.load_app(OpenwrtLogAnalyzerConfig) + for ap in config.app_config['aps']: + dbname = config.app_config['database_name'] + dbname = dbname.replace('.txt', f'-{ap}.txt') + + state = SimpleState(name=dbname, + default={'last_id': 0}) + + max_last_id = 0 + for name, mac in config['devices'].items(): + last_id = main(mac, title=name, ap=ap) + if last_id > max_last_id: + max_last_id = last_id + + if max_last_id: + state['last_id'] = max_last_id diff --git a/src/openwrt_logger.py b/bin/openwrt_logger.py index 97fe7a9..ec67542 100755 --- a/src/openwrt_logger.py +++ b/bin/openwrt_logger.py @@ -1,30 +1,21 @@ #!/usr/bin/env python3 import os +import __py_include from datetime import datetime -from typing import Tuple, List +from typing import Tuple, List, Optional from argparse import ArgumentParser -from home.config import config -from home.database import SimpleState -from home.api import WebAPIClient +from homekit.config import config, AppConfigUnit +from homekit.database import SimpleState +from homekit.api import WebApiClient -f""" -This script is supposed to be run by cron every 5 minutes or so. -It looks for new lines in log file and sends them to remote server. -OpenWRT must have remote logging enabled (UDP; IP of host this script is launched on; port 514) - -/etc/rsyslog.conf contains following (assuming 192.168.1.1 is the router IP): - -$ModLoad imudp -$UDPServerRun 514 -:fromhost-ip, isequal, "192.168.1.1" /var/log/openwrt.log -& ~ - -Also comment out the following line: -$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat - -""" +class OpenwrtLoggerConfig(AppConfigUnit): + @classmethod + def schema(cls) -> Optional[dict]: + return dict( + database_name_template=dict(type='string', required=True) + ) def parse_line(line: str) -> Tuple[int, str]: @@ -46,11 +37,10 @@ if __name__ == '__main__': parser.add_argument('--access-point', type=int, required=True, help='access point number') - arg = config.load_app('openwrt_logger', parser=parser) - - state = SimpleState(file=config['simple_state']['file'].replace('{ap}', str(arg.access_point)), - default={'seek': 0, 'size': 0}) + arg = config.load_app(OpenwrtLoggerConfig, parser=parser) + state = SimpleState(name=config.app_config['database_name_template'].replace('{ap}', str(arg.access_point)), + default=dict(seek=0, size=0)) fsize = os.path.getsize(arg.file) if fsize < state['size']: state['seek'] = 0 @@ -79,5 +69,5 @@ if __name__ == '__main__': except ValueError: lines.append((0, line)) - api = WebAPIClient() + api = WebApiClient() api.log_openwrt(lines, arg.access_point) diff --git a/src/pio_build.py b/bin/pio_build.py index 1916e5e..539df44 100644 --- a/src/pio_build.py +++ b/bin/pio_build.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import __py_include if __name__ == '__main__': print('TODO')
\ No newline at end of file diff --git a/src/pio_ini.py b/bin/pio_ini.py index 920c3e5..ee85732 100755 --- a/src/pio_ini.py +++ b/bin/pio_ini.py @@ -2,22 +2,24 @@ import os import yaml import re +import __py_include -from pprint import pprint from argparse import ArgumentParser, ArgumentError -from home.pio import get_products, platformio_ini -from home.pio.exceptions import ProductConfigNotFoundError +from homekit.pio import get_products, platformio_ini +from homekit.pio.exceptions import ProductConfigNotFoundError +from homekit.config import CONFIG_DIRECTORIES def get_config(product: str) -> dict: - config_path = os.path.join( - os.getenv('HOME'), '.config', - 'homekit_pio', f'{product}.yaml' - ) - if not os.path.exists(config_path): - raise ProductConfigNotFoundError(f'{config_path}: product config not found') - - with open(config_path, 'r') as f: + path = None + for directory in CONFIG_DIRECTORIES: + config_path = os.path.join(directory, 'pio', f'{product}.yaml') + if os.path.exists(config_path) and os.path.isfile(config_path): + path = config_path + break + if not path: + raise ProductConfigNotFoundError(f'pio/{product}.yaml not found') + with open(path, 'r') as f: return yaml.safe_load(f) @@ -82,7 +84,8 @@ def bsd_get(product_config: dict, defines[f'CONFIG_{define_name}'] = f'HOMEKIT_{attr_value.upper()}' return if kwargs['type'] == 'bool': - defines[f'CONFIG_{define_name}'] = True + if attr_value is True: + defines[f'CONFIG_{define_name}'] = True return defines[f'CONFIG_{define_name}'] = str(attr_value) bsd_walk(product_config, f) @@ -106,7 +109,7 @@ if __name__ == '__main__': product_config = get_config(product) - # then everythingm else + # then everything else parser = ArgumentParser(parents=[product_parser]) parser.add_argument('--target', type=str, required=True, choices=product_config['targets'], help='PIO build target') @@ -123,6 +126,7 @@ if __name__ == '__main__': raise ArgumentError(None, f'target {arg.target} not found for product {product}') bsd, bsd_enums = bsd_get(product_config, arg) + ini = platformio_ini(product_config=product_config, target=arg.target, build_specific_defines=bsd, diff --git a/src/polaris_kettle_bot.py b/bin/polaris_kettle_bot.py index 80baef3..05c2aae 100755 --- a/src/polaris_kettle_bot.py +++ b/bin/polaris_kettle_bot.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import __py_include import logging import locale import queue @@ -8,11 +9,10 @@ import time import threading import paho.mqtt.client as mqtt -from home.telegram import bot -from home.api.types import BotType -from home.mqtt import Mqtt -from home.config import config -from home.util import chunks +from homekit.telegram import bot +from homekit.mqtt import Mqtt +from homekit.config import config +from homekit.util import chunks from syncleo import ( Kettle, PowerType, @@ -737,9 +737,6 @@ if __name__ == '__main__': kc = KettleController() - if 'api' in config: - bot.enable_logging(BotType.POLARIS_KETTLE) - bot.run() # bot library handles signals, so when sigterm or something like that happens, we should stop all other threads here diff --git a/src/polaris_kettle_util.py b/bin/polaris_kettle_util.py index 12c4388..4db0ed4 100755 --- a/src/polaris_kettle_util.py +++ b/bin/polaris_kettle_util.py @@ -4,12 +4,13 @@ import logging import sys import paho.mqtt.client as mqtt +import __py_include from typing import Optional from argparse import ArgumentParser from queue import SimpleQueue -from home.mqtt import Mqtt -from home.config import config +from homekit.mqtt import Mqtt +from homekit.config import config from syncleo import ( Kettle, PowerType, diff --git a/src/pump_bot.py b/bin/pump_bot.py index 172108e..e00e844 100755 --- a/src/pump_bot.py +++ b/bin/pump_bot.py @@ -1,26 +1,62 @@ #!/usr/bin/env python3 +import __py_include +import sys +import asyncio + from enum import Enum -from typing import Optional +from typing import Optional, Union from telegram import ReplyKeyboardMarkup, User from time import time from datetime import datetime -from home.config import config, is_development_mode -from home.telegram import bot -from home.telegram._botutil import user_any_name -from home.relay.sunxi_h3_client import RelayClient -from home.api.types import BotType -from home.mqtt import MqttNode, MqttWrapper, MqttPayload -from home.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule -from home.mqtt.module.temphum import MqttTemphumDataPayload -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config, is_development_mode, AppConfigUnit +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig, TelegramUserListType +from homekit.telegram._botutil import user_any_name +from homekit.relay.sunxi_h3_client import RelayClient +from homekit.mqtt import MqttNode, MqttWrapper, MqttPayload, MqttNodesConfig, MqttModule +from homekit.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule +from homekit.mqtt.module.temphum import MqttTemphumDataPayload +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload + + +if __name__ != '__main__': + print(f'this script can not be imported as module', file=sys.stderr) + sys.exit(1) + + +mqtt_nodes_config = MqttNodesConfig() + + +class PumpBotUserListType(TelegramUserListType): + SILENT = 'silent_users' + +class PumpBotConfig(AppConfigUnit, TelegramBotConfig): + NAME = 'pump_bot' -config.load_app('pump_bot') + @classmethod + def schema(cls) -> Optional[dict]: + return { + **super(TelegramBotConfig).schema(), + PumpBotUserListType.SILENT: TelegramBotConfig._userlist_schema(), + 'watering_relay_node': {'type': 'string'}, + 'pump_relay_addr': cls._addr_schema() + } + + @staticmethod + def custom_validator(data): + relay_node_names = mqtt_nodes_config.get_nodes(filters=('relay',), only_names=True) + if data['watering_relay_node'] not in relay_node_names: + raise ValueError(f'unknown relay node "{data["watering_relay_node"]}"') + + +config.load_app(PumpBotConfig) + +mqtt: MqttWrapper +mqtt_node: MqttNode +mqtt_relay_module: Union[MqttRelayModule, MqttModule] -mqtt: Optional[MqttWrapper] = None -mqtt_node: Optional[MqttNode] = None -mqtt_relay_module: Optional[MqttRelayModule] = None time_format = '%d.%m.%Y, %H:%M:%S' watering_mcu_status = { @@ -98,81 +134,89 @@ class UserAction(Enum): def get_relay() -> RelayClient: - relay = RelayClient(host=config['relay']['ip'], port=config['relay']['port']) + relay = RelayClient(host=config.app_config['pump_relay_addr'].host, + port=config.app_config['pump_relay_addr'].port) relay.connect() return relay -def on(ctx: bot.Context, silent=False) -> None: +async def on(ctx: bot.Context, silent=False) -> None: get_relay().on() - ctx.reply(ctx.lang('done')) + futures = [ctx.reply(ctx.lang('done'))] if not silent: - notify(ctx.user, UserAction.ON) + futures.append(notify(ctx.user, UserAction.ON)) + await asyncio.gather(*futures) -def off(ctx: bot.Context, silent=False) -> None: +async def off(ctx: bot.Context, silent=False) -> None: get_relay().off() - ctx.reply(ctx.lang('done')) + futures = [ctx.reply(ctx.lang('done'))] if not silent: - notify(ctx.user, UserAction.OFF) + futures.append(notify(ctx.user, UserAction.OFF)) + await asyncio.gather(*futures) -def watering_on(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(True, config.get('mqtt_water_relay.secret')) - ctx.reply(ctx.lang('sent')) - notify(ctx.user, UserAction.WATERING_ON) +async def watering_on(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(True) + await asyncio.gather( + ctx.reply(ctx.lang('sent')), + notify(ctx.user, UserAction.WATERING_ON) + ) -def watering_off(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(False, config.get('mqtt_water_relay.secret')) - ctx.reply(ctx.lang('sent')) - notify(ctx.user, UserAction.WATERING_OFF) +async def watering_off(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(False) + await asyncio.gather( + ctx.reply(ctx.lang('sent')), + notify(ctx.user, UserAction.WATERING_OFF) + ) -def notify(user: User, action: UserAction) -> None: +async def notify(user: User, action: UserAction) -> None: notification_key = 'user_watering_notification' if action in (UserAction.WATERING_ON, UserAction.WATERING_OFF) else 'user_action_notification' + def text_getter(lang: str): action_name = bot.lang.get(f'user_action_{action.value}', lang) user_name = user_any_name(user) return 'ℹ ' + bot.lang.get(notification_key, lang, user.id, user_name, action_name) - bot.notify_all(text_getter, exclude=(user.id,)) + await bot.notify_all(text_getter, exclude=(user.id,)) @bot.handler(message='enable') -def enable_handler(ctx: bot.Context) -> None: - on(ctx) +async def enable_handler(ctx: bot.Context) -> None: + await on(ctx) @bot.handler(message='enable_silently') -def enable_s_handler(ctx: bot.Context) -> None: - on(ctx, True) +async def enable_s_handler(ctx: bot.Context) -> None: + await on(ctx, True) @bot.handler(message='disable') -def disable_handler(ctx: bot.Context) -> None: - off(ctx) +async def disable_handler(ctx: bot.Context) -> None: + await off(ctx) @bot.handler(message='start_watering') -def start_watering(ctx: bot.Context) -> None: - watering_on(ctx) +async def start_watering(ctx: bot.Context) -> None: + await watering_on(ctx) @bot.handler(message='stop_watering') -def stop_watering(ctx: bot.Context) -> None: - watering_off(ctx) +async def stop_watering(ctx: bot.Context) -> None: + await watering_off(ctx) @bot.handler(message='disable_silently') -def disable_s_handler(ctx: bot.Context) -> None: - off(ctx, True) +async def disable_s_handler(ctx: bot.Context) -> None: + await off(ctx, True) @bot.handler(message='status') -def status(ctx: bot.Context) -> None: - ctx.reply( +async def status(ctx: bot.Context) -> None: + await ctx.reply( ctx.lang('enabled') if get_relay().status() == 'on' else ctx.lang('disabled') ) @@ -185,7 +229,7 @@ def _get_timestamp_as_string(timestamp: int) -> str: @bot.handler(message='watering_status') -def watering_status(ctx: bot.Context) -> None: +async def watering_status(ctx: bot.Context) -> None: buf = '' if 0 < watering_mcu_status["last_time"] < time()-1800: buf += '<b>WARNING! long time no reports from mcu! maybe something\'s wrong</b>\n' @@ -194,13 +238,13 @@ def watering_status(ctx: bot.Context) -> None: buf += f'boot time: <b>{_get_timestamp_as_string(watering_mcu_status["last_boot_time"])}</b>\n' buf += 'relay opened: <b>' + ('yes' if watering_mcu_status['relay_opened'] else 'no') + '</b>\n' buf += f'ambient temp & humidity: <b>{watering_mcu_status["ambient_temp"]} °C, {watering_mcu_status["ambient_rh"]}%</b>' - ctx.reply(buf) + await ctx.reply(buf) @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: buttons = [] - if ctx.user_id in config['bot']['silent_users']: + if ctx.user_id in config.app_config.get_user_ids(PumpBotUserListType.SILENT): buttons.append([ctx.lang('enable_silently'), ctx.lang('disable_silently')]) buttons.append([ctx.lang('enable'), ctx.lang('disable'), ctx.lang('status')],) buttons.append([ctx.lang('start_watering'), ctx.lang('stop_watering'), ctx.lang('watering_status')]) @@ -233,24 +277,21 @@ def mqtt_payload_callback(mqtt_node: MqttNode, payload: MqttPayload): watering_mcu_status['relay_opened'] = payload.opened -if __name__ == '__main__': - mqtt = MqttWrapper() - mqtt_node = MqttNode(node_id=config.get('mqtt_water_relay.node_id')) - if is_development_mode(): - mqtt_node.load_module('diagnostics') +mqtt = MqttWrapper(client_id='pump_bot') +mqtt_node = MqttNode(node_id=config.app_config['watering_relay_node']) +if is_development_mode(): + mqtt_node.load_module('diagnostics') - mqtt_node.load_module('temphum') - mqtt_relay_module = mqtt_node.load_module('relay') +mqtt_node.load_module('temphum') +mqtt_relay_module = mqtt_node.load_module('relay') - mqtt_node.add_payload_callback(mqtt_payload_callback) +mqtt_node.add_payload_callback(mqtt_payload_callback) - mqtt.configure_tls() - mqtt.connect_and_loop(loop_forever=False) +mqtt.connect_and_loop(loop_forever=False) - bot.enable_logging(BotType.PUMP) - bot.run() +bot.run() - try: - mqtt.disconnect() - except: - pass +try: + mqtt.disconnect() +except: + pass diff --git a/src/pump_mqtt_bot.py b/bin/pump_mqtt_bot.py index 1c52b03..aea1451 100755 --- a/src/pump_mqtt_bot.py +++ b/bin/pump_mqtt_bot.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 import datetime +import __py_include from enum import Enum from typing import Optional from telegram import ReplyKeyboardMarkup, User -from home.config import config -from home.telegram import bot -from home.telegram._botutil import user_any_name -from home.mqtt import MqttNode, MqttPayload -from home.mqtt.module.relay import MqttRelayState -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config +from homekit.telegram import bot +from homekit.telegram._botutil import user_any_name +from homekit.mqtt import MqttNode, MqttPayload +from homekit.mqtt.module.relay import MqttRelayState +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload config.load_app('pump_mqtt_bot') @@ -159,7 +160,6 @@ if __name__ == '__main__': mqtt = MqttRelay(devices=MqttEspDevice(id=config['mqtt']['home_id'], secret=config['mqtt']['home_secret'])) mqtt.set_message_callback(on_mqtt_message) - mqtt.configure_tls() mqtt.connect_and_loop(loop_forever=False) # bot.enable_logging(BotType.PUMP_MQTT) diff --git a/src/relay_mqtt_bot.py b/bin/relay_mqtt_bot.py index 2fb9d24..3ad0a9b 100755 --- a/src/relay_mqtt_bot.py +++ b/bin/relay_mqtt_bot.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 import sys +import __py_include from enum import Enum from typing import Optional, Union from telegram import ReplyKeyboardMarkup from functools import partial -from home.config import config, AppConfigUnit, TranslationsUnit -from home.telegram import bot -from home.telegram.config import TelegramBotConfig -from home.mqtt import MqttPayload, MqttNode, MqttWrapper, MqttModule -from home.mqtt import MqttNodesConfig -from home.mqtt.module.relay import MqttRelayModule, MqttRelayState -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config, AppConfigUnit, Translation +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig +from homekit.mqtt import MqttPayload, MqttNode, MqttWrapper, MqttModule, MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayModule, MqttRelayState +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload if __name__ != '__main__': @@ -26,12 +26,14 @@ mqtt_nodes_config = MqttNodesConfig() class RelayMqttBotConfig(AppConfigUnit, TelegramBotConfig): NAME = 'relay_mqtt_bot' + _strings: Translation + def __init__(self): super().__init__() - self._mqtt_nodes_strings = TranslationsUnit('mqtt_nodes') + self._strings = Translation('mqtt_nodes') - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { **super(TelegramBotConfig).schema(), 'relay_nodes': { @@ -51,7 +53,7 @@ class RelayMqttBotConfig(AppConfigUnit, TelegramBotConfig): raise ValueError(f'unknown relay node "{node}"') def get_relay_name_translated(self, lang: str, relay_name: str) -> str: - pass + return self._strings.get(lang)[relay_name]['relay'] config.load_app(RelayMqttBotConfig) @@ -78,7 +80,7 @@ status_emoji = { } -mqtt: Optional[MqttWrapper] = None +mqtt: MqttWrapper relay_nodes: dict[str, Union[MqttRelayModule, MqttModule]] = {} relay_states: dict[str, MqttRelayState] = {} @@ -99,32 +101,32 @@ def on_mqtt_message(node: MqttNode, relay_states[node.id].update(**kwargs) -def enable_handler(node_id: str, ctx: bot.Context) -> None: +async def enable_handler(node_id: str, ctx: bot.Context) -> None: relay_nodes[node_id].switchpower(True) - ctx.reply(ctx.lang('done')) + await ctx.reply(ctx.lang('done')) -def disable_handler(node_id: str, ctx: bot.Context) -> None: +async def disable_handler(node_id: str, ctx: bot.Context) -> None: relay_nodes[node_id].switchpower(False) - ctx.reply(ctx.lang('done')) + await ctx.reply(ctx.lang('done')) -def start(ctx: bot.Context) -> None: - ctx.reply(ctx.lang('start_message')) +async def start(ctx: bot.Context) -> None: + await ctx.reply(ctx.lang('start_message')) @bot.exceptionhandler -def exception_handler(e: Exception, ctx: bot.Context) -> bool: +async def exception_handler(e: Exception, ctx: bot.Context) -> bool: return False @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: buttons = [] - for device_id, data in config['relays'].items(): - labels = data['labels'] - type_emoji = type_emojis[data['type']] - row = [f'{type_emoji}{status_emoji[i.value]} {labels[ctx.user_lang]}' + for node_id in config.app_config['relay_nodes']: + node_data = mqtt_nodes_config.get_node(node_id) + type_emoji = type_emojis[node_data['relay']['device_type']] + row = [f'{type_emoji}{status_emoji[i.value]} {config.app_config.get_relay_name_translated(ctx.user_lang, node_id)}' for i in UserAction] buttons.append(row) return ReplyKeyboardMarkup(buttons, one_time_keyboard=False) @@ -132,25 +134,29 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: devices = [] mqtt = MqttWrapper(client_id='relay_mqtt_bot') -for device_id, data in config['relays'].items(): - mqtt_node = MqttNode(node_id=device_id, node_secret=data['secret']) - relay_nodes[device_id] = mqtt_node.load_module('relay') +for node_id in config.app_config['relay_nodes']: + node_data = mqtt_nodes_config.get_node(node_id) + mqtt_node = MqttNode(node_id=node_id, + node_secret=node_data['password']) + module_kwargs = {} + try: + if node_data['relay']['legacy_topics']: + module_kwargs['legacy_topics'] = True + except KeyError: + pass + relay_nodes[node_id] = mqtt_node.load_module('relay', **module_kwargs) mqtt_node.add_payload_callback(on_mqtt_message) mqtt.add_node(mqtt_node) - labels = data['labels'] - bot.lang.ru(**{device_id: labels['ru']}) - bot.lang.en(**{device_id: labels['en']}) - - type_emoji = type_emojis[data['type']] + type_emoji = type_emojis[node_data['relay']['device_type']] for action in UserAction: messages = [] - for _lang, _label in labels.items(): - messages.append(f'{type_emoji}{status_emoji[action.value]} {labels[_lang]}') - bot.handler(texts=messages)(partial(enable_handler if action == UserAction.ON else disable_handler, device_id)) + for _lang in Translation.LANGUAGES: + _label = config.app_config.get_relay_name_translated(_lang, node_id) + messages.append(f'{type_emoji}{status_emoji[action.value]} {_label}') + bot.handler(texts=messages)(partial(enable_handler if action == UserAction.ON else disable_handler, node_id)) -mqtt.configure_tls() mqtt.connect_and_loop(loop_forever=False) bot.run(start_handler=start) diff --git a/bin/relay_mqtt_http_proxy.py b/bin/relay_mqtt_http_proxy.py new file mode 100755 index 0000000..23938e1 --- /dev/null +++ b/bin/relay_mqtt_http_proxy.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import logging +import __py_include + +from homekit import http +from homekit.config import config, AppConfigUnit +from homekit.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayState, MqttRelayModule, MqttPowerStatusPayload +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from typing import Optional, Union + + +logger = logging.getLogger(__name__) +mqtt: Optional[MqttWrapper] = None +mqtt_nodes: dict[str, MqttNode] = {} +relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {} +relay_states: dict[str, MqttRelayState] = {} + +mqtt_nodes_config = MqttNodesConfig() + + +class RelayMqttHttpProxyConfig(AppConfigUnit): + NAME = 'relay_mqtt_http_proxy' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'relay_nodes': { + 'type': 'list', + 'required': True, + 'schema': { + 'type': 'string' + } + }, + 'listen_addr': cls._addr_schema(required=True) + } + + @staticmethod + def custom_validator(data): + relay_node_names = mqtt_nodes_config.get_nodes(filters=('relay',), only_names=True) + for node in data['relay_nodes']: + if node not in relay_node_names: + raise ValueError(f'unknown relay node "{node}"') + + +def on_mqtt_message(node: MqttNode, + message: MqttPayload): + try: + is_legacy = mqtt_nodes_config[node.id]['relay']['legacy_topics'] + logger.debug(f'on_mqtt_message: relay {node.id} uses legacy topic names') + except KeyError: + is_legacy = False + kwargs = {} + + if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): + kwargs['rssi'] = message.rssi + if is_legacy: + kwargs['enabled'] = message.flags.state + + if not is_legacy and isinstance(message, MqttPowerStatusPayload): + kwargs['enabled'] = message.opened + + if len(kwargs): + logger.debug(f'on_mqtt_message: {node.id}: going to update relay state: {str(kwargs)}') + if node.id not in relay_states: + relay_states[node.id] = MqttRelayState() + relay_states[node.id].update(**kwargs) + + +class RelayMqttHttpProxy(http.HTTPServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.get('/relay/{id}/on', self.relay_on) + self.get('/relay/{id}/off', self.relay_off) + self.get('/relay/{id}/toggle', self.relay_toggle) + + async def _relay_on_off(self, + enable: Optional[bool], + req: http.Request): + node_id = req.match_info['id'] + node_secret = req.query['secret'] + + node = mqtt_nodes[node_id] + relay_module = relay_modules[node_id] + + if enable is None: + if node_id in relay_states and relay_states[node_id].ever_updated: + cur_state = relay_states[node_id].enabled + else: + cur_state = False + enable = not cur_state + + node.secret = node_secret + relay_module.switchpower(enable) + return self.ok() + + async def relay_on(self, req: http.Request): + return await self._relay_on_off(True, req) + + async def relay_off(self, req: http.Request): + return await self._relay_on_off(False, req) + + async def relay_toggle(self, req: http.Request): + return await self._relay_on_off(None, req) + + +if __name__ == '__main__': + config.load_app(RelayMqttHttpProxyConfig) + + mqtt = MqttWrapper(client_id='relay_mqtt_http_proxy', + randomize_client_id=True) + for node_id in config.app_config['relay_nodes']: + node_data = mqtt_nodes_config.get_node(node_id) + mqtt_node = MqttNode(node_id=node_id) + module_kwargs = {} + try: + if node_data['relay']['legacy_topics']: + module_kwargs['legacy_topics'] = True + except KeyError: + pass + relay_modules[node_id] = mqtt_node.load_module('relay', **module_kwargs) + if 'legacy_topics' in module_kwargs: + mqtt_node.load_module('diagnostics') + mqtt_node.add_payload_callback(on_mqtt_message) + mqtt.add_node(mqtt_node) + mqtt_nodes[node_id] = mqtt_node + + mqtt.connect_and_loop(loop_forever=False) + + proxy = RelayMqttHttpProxy(config.app_config['listen_addr']) + try: + proxy.run() + except KeyboardInterrupt: + mqtt.disconnect() diff --git a/src/sensors_bot.py b/bin/sensors_bot.py index 152dd24..43932e1 100755 --- a/src/sensors_bot.py +++ b/bin/sensors_bot.py @@ -4,6 +4,7 @@ import socket import logging import re import gc +import __py_include from io import BytesIO from typing import Optional @@ -14,12 +15,11 @@ import matplotlib.ticker as mticker from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton -from home.config import config -from home.telegram import bot -from home.util import chunks, MySimpleSocketClient -from home.api import WebAPIClient -from home.api.types import ( - BotType, +from homekit.config import config +from homekit.telegram import bot +from homekit.util import chunks, MySimpleSocketClient +from homekit.api import WebApiClient +from homekit.api.types import ( TemperatureSensorLocation ) @@ -111,7 +111,7 @@ def callback_handler(ctx: bot.Context) -> None: sensor = TemperatureSensorLocation[match.group(1).upper()] hours = int(match.group(2)) - api = WebAPIClient(timeout=20) + api = WebApiClient(timeout=20) data = api.get_sensors_data(sensor, hours) title = ctx.lang(sensor.name.lower()) + ' (' + ctx.lang('n_hrs', hours) + ')' @@ -175,7 +175,4 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: if __name__ == '__main__': - if 'api' in config: - bot.enable_logging(BotType.SENSORS) - bot.run() diff --git a/src/sound_bot.py b/bin/sound_bot.py index a2f8342..fa22ba7 100755 --- a/src/sound_bot.py +++ b/bin/sound_bot.py @@ -2,21 +2,22 @@ import logging import os import tempfile +import __py_include from enum import Enum from datetime import datetime, timedelta from html import escape from typing import Optional, List, Dict, Tuple -from home.config import config -from home.api import WebAPIClient -from home.api.types import SoundSensorLocation, BotType -from home.api.errors import ApiResponseError -from home.media import SoundNodeClient, SoundRecordClient, SoundRecordFile, CameraNodeClient -from home.soundsensor import SoundSensorServerGuardClient -from home.util import parse_addr, chunks, filesize_fmt +from homekit.config import config +from homekit.api import WebApiClient +from homekit.api.types import SoundSensorLocation +from homekit.api.errors import ApiResponseError +from homekit.media import SoundNodeClient, SoundRecordClient, SoundRecordFile, CameraNodeClient +from homekit.soundsensor import SoundSensorServerGuardClient +from homekit.util import Addr, chunks, filesize_fmt -from home.telegram import bot +from homekit.telegram import bot from telegram.error import TelegramError from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton, User @@ -27,7 +28,7 @@ config.load_app('sound_bot') nodes = {} for nodename, nodecfg in config['nodes'].items(): - nodes[nodename] = parse_addr(nodecfg['addr']) + nodes[nodename] = Addr.fromstring(nodecfg['addr']) bot.initialize() bot.lang.ru( @@ -142,13 +143,13 @@ cam_client_links: Dict[str, CameraNodeClient] = {} def node_client(node: str) -> SoundNodeClient: if node not in node_client_links: - node_client_links[node] = SoundNodeClient(parse_addr(config['nodes'][node]['addr'])) + node_client_links[node] = SoundNodeClient(Addr.fromstring(config['nodes'][node]['addr'])) return node_client_links[node] def camera_client(cam: str) -> CameraNodeClient: if cam not in node_client_links: - cam_client_links[cam] = CameraNodeClient(parse_addr(config['cameras'][cam]['addr'])) + cam_client_links[cam] = CameraNodeClient(Addr.fromstring(config['cameras'][cam]['addr'])) return cam_client_links[cam] @@ -188,7 +189,7 @@ def manual_recording_allowed(user_id: int) -> bool: def guard_client() -> SoundSensorServerGuardClient: - return SoundSensorServerGuardClient(parse_addr(config['bot']['guard_server'])) + return SoundSensorServerGuardClient(Addr.fromstring(config['bot']['guard_server'])) # message renderers @@ -734,7 +735,7 @@ def sound_sensors_last_24h(ctx: bot.Context): ctx.answer() - cl = WebAPIClient() + cl = WebApiClient() data = cl.get_sound_sensor_hits(location=SoundSensorLocation[node.upper()], after=datetime.now() - timedelta(hours=24)) @@ -757,7 +758,7 @@ def sound_sensors_last_anything(ctx: bot.Context): ctx.answer() - cl = WebAPIClient() + cl = WebApiClient() data = cl.get_last_sound_sensor_hits(location=SoundSensorLocation[node.upper()], last=20) @@ -883,7 +884,5 @@ if __name__ == '__main__': finished_handler=record_onfinished, download_on_finish=True) - if 'api' in config: - bot.enable_logging(BotType.SOUND) bot.run() record_client.stop() diff --git a/src/sound_node.py b/bin/sound_node.py index b0b4a67..90e6997 100755 --- a/src/sound_node.py +++ b/bin/sound_node.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import os +import __py_include from typing import Optional -from home.config import config -from home.audio import amixer -from home.media import MediaNodeServer, SoundRecordStorage, SoundRecorder -from home import http +from homekit.config import config +from homekit.audio import amixer +from homekit.media import MediaNodeServer, SoundRecordStorage, SoundRecorder +from homekit import http # This script must be run as root as it runs arecord. diff --git a/src/sound_sensor_node.py b/bin/sound_sensor_node.py index e332174..39c3905 100755 --- a/src/sound_sensor_node.py +++ b/bin/sound_sensor_node.py @@ -2,10 +2,11 @@ import logging import os import sys +import __py_include -from home.config import config -from home.util import parse_addr -from home.soundsensor import SoundSensorNode +from homekit.config import config +from homekit.util import Addr +from homekit.soundsensor import SoundSensorNode logger = logging.getLogger(__name__) @@ -21,7 +22,7 @@ if __name__ == '__main__': kwargs['delay'] = config['node']['delay'] if 'server_addr' in config['node']: - server_addr = parse_addr(config['node']['server_addr']) + server_addr = Addr.fromstring(config['node']['server_addr']) else: server_addr = None diff --git a/src/sound_sensor_server.py b/bin/sound_sensor_server.py index 3a68a08..fd7ff5a 100755 --- a/src/sound_sensor_server.py +++ b/bin/sound_sensor_server.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 import logging import threading +import __py_include from time import sleep from typing import Optional, List, Dict, Tuple from functools import partial -from home.config import config -from home.util import parse_addr -from home.api import WebAPIClient, RequestParams -from home.api.types import SoundSensorLocation -from home.soundsensor import SoundSensorServer, SoundSensorHitHandler -from home.media import MediaNodeType, SoundRecordClient, CameraRecordClient, RecordClient +from homekit.config import config +from homekit.util import Addr +from homekit.api import WebApiClient, RequestParams +from homekit.api.types import SoundSensorLocation +from homekit.soundsensor import SoundSensorServer, SoundSensorHitHandler +from homekit.media import MediaNodeType, SoundRecordClient, CameraRecordClient, RecordClient interrupted = False logger = logging.getLogger(__name__) @@ -120,7 +121,7 @@ def hits_sender(): sleep(5) -api: Optional[WebAPIClient] = None +api: Optional[WebApiClient] = None hc: Optional[HitCounter] = None record_clients: Dict[MediaNodeType, RecordClient] = {} @@ -162,7 +163,7 @@ if __name__ == '__main__': config.load_app('sound_sensor_server') hc = HitCounter() - api = WebAPIClient(timeout=(10, 60)) + api = WebApiClient(timeout=(10, 60)) api.enable_async(error_handler=api_error_handler) t = threading.Thread(target=hits_sender) @@ -172,12 +173,12 @@ if __name__ == '__main__': sound_nodes = {} if 'sound_nodes' in config: for nodename, nodecfg in config['sound_nodes'].items(): - sound_nodes[nodename] = parse_addr(nodecfg['addr']) + sound_nodes[nodename] = Addr.fromstring(nodecfg['addr']) camera_nodes = {} if 'camera_nodes' in config: for nodename, nodecfg in config['camera_nodes'].items(): - camera_nodes[nodename] = parse_addr(nodecfg['addr']) + camera_nodes[nodename] = Addr.fromstring(nodecfg['addr']) if sound_nodes: record_clients[MediaNodeType.SOUND] = SoundRecordClient(sound_nodes, diff --git a/src/ssh_tunnels_config_util.py b/bin/ssh_tunnels_config_util.py index 03a8219..d08a4f4 100755 --- a/src/ssh_tunnels_config_util.py +++ b/bin/ssh_tunnels_config_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 - -from home.config import config +import __py_include +from homekit.config import config if __name__ == '__main__': config.load_app('ssh_tunnels_config_util') @@ -8,7 +8,7 @@ if __name__ == '__main__': network_prefix = config['network'] hostnames = [] - for k, v in config.items(): + for k, v in config.app_config.get().items(): if type(v) is not dict: continue hostnames.append(k) diff --git a/src/temphum_mqtt_node.py b/bin/temphum_mqtt_node.py index c3d1975..9ea436d 100755 --- a/src/temphum_mqtt_node.py +++ b/bin/temphum_mqtt_node.py @@ -2,12 +2,13 @@ import asyncio import json import logging +import __py_include from typing import Optional -from home.config import config -from home.temphum import SensorType, BaseSensor -from home.temphum.i2c import create_sensor +from homekit.config import config +from homekit.temphum import SensorType, BaseSensor +from homekit.temphum.i2c import create_sensor logger = logging.getLogger(__name__) sensor: Optional[BaseSensor] = None diff --git a/src/temphum_mqtt_receiver.py b/bin/temphum_mqtt_receiver.py index a4b888e..e9ee397 100755 --- a/src/temphum_mqtt_receiver.py +++ b/bin/temphum_mqtt_receiver.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 import paho.mqtt.client as mqtt import re +import __py_include -from home.config import config -from home.mqtt import MqttWrapper, MqttNode +from homekit.config import config +from homekit.mqtt import MqttWrapper, MqttNode class MqttServer(Mqtt): @@ -44,5 +45,4 @@ if __name__ == '__main__': node.load_module('temphum', write_to_database=True) mqtt.add_node(node) - mqtt.configure_tls() - mqtt.connect_and_loop()
\ No newline at end of file + mqtt.connect_and_loop() diff --git a/src/temphum_nodes_util.py b/bin/temphum_nodes_util.py index c700ca8..aa46494 100755 --- a/src/temphum_nodes_util.py +++ b/bin/temphum_nodes_util.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -from home.mqtt.temphum import MqttTempHumNodes +import __py_include + +from homekit.mqtt.temphum import MqttTempHumNodes if __name__ == '__main__': max_name_len = 0 diff --git a/src/temphum_smbus_util.py b/bin/temphum_smbus_util.py index c06bacd..1cfaa84 100755 --- a/src/temphum_smbus_util.py +++ b/bin/temphum_smbus_util.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import __py_include + from argparse import ArgumentParser -from home.temphum import SensorType -from home.temphum.i2c import create_sensor +from homekit.temphum import SensorType +from homekit.temphum.i2c import create_sensor if __name__ == '__main__': diff --git a/src/temphumd.py b/bin/temphumd.py index c3d1975..9ea436d 100755 --- a/src/temphumd.py +++ b/bin/temphumd.py @@ -2,12 +2,13 @@ import asyncio import json import logging +import __py_include from typing import Optional -from home.config import config -from home.temphum import SensorType, BaseSensor -from home.temphum.i2c import create_sensor +from homekit.config import config +from homekit.temphum import SensorType, BaseSensor +from homekit.temphum.i2c import create_sensor logger = logging.getLogger(__name__) sensor: Optional[BaseSensor] = None diff --git a/src/web_api.py b/bin/web_api.py index 0aa994a..d221838 100755 --- a/src/web_api.py +++ b/bin/web_api.py @@ -2,16 +2,17 @@ import asyncio import json import os +import __py_include from datetime import datetime, timedelta from aiohttp import web -from home import http -from home.config import config, is_development_mode -from home.database import BotsDatabase, SensorsDatabase, InverterDatabase -from home.database.inverter_time_formats import * -from home.api.types import BotType, TemperatureSensorLocation, SoundSensorLocation -from home.media import SoundRecordStorage +from homekit import http +from homekit.config import config, is_development_mode +from homekit.database import BotsDatabase, SensorsDatabase, InverterDatabase +from homekit.database.inverter_time_formats import * +from homekit.api.types import TemperatureSensorLocation, SoundSensorLocation +from homekit.media import SoundRecordStorage def strptime_auto(s: str) -> datetime: @@ -41,7 +42,6 @@ class WebAPIServer(http.HTTPServer): self.get('/sound_sensors/hits/', self.GET_sound_sensors_hits) self.post('/sound_sensors/hits/', self.POST_sound_sensors_hits) - self.post('/log/bot_request/', self.POST_bot_request_log) self.post('/log/openwrt/', self.POST_openwrt_log) self.get('/inverter/consumed_energy/', self.GET_consumed_energy) @@ -125,30 +125,6 @@ class WebAPIServer(http.HTTPServer): BotsDatabase().add_sound_hits(hits, datetime.now()) return self.ok() - async def POST_bot_request_log(self, req: http.Request): - data = await req.post() - - try: - user_id = int(data['user_id']) - except KeyError: - user_id = 0 - - try: - message = data['message'] - except KeyError: - message = '' - - bot = BotType(int(data['bot'])) - - # validate message - if message.strip() == '': - raise ValueError('message can\'t be empty') - - # add record to the database - BotsDatabase().add_request(bot, user_id, message) - - return self.ok() - async def POST_openwrt_log(self, req: http.Request): data = await req.post() diff --git a/bin/web_kbn.py b/bin/web_kbn.py new file mode 100644 index 0000000..c21269b --- /dev/null +++ b/bin/web_kbn.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +import asyncio +import jinja2 +import aiohttp_jinja2 +import json +import re +import inverterd +import phonenumbers +import __py_include + +from io import StringIO +from aiohttp.web import HTTPFound +from typing import Optional, Union +from homekit.config import config, AppConfigUnit +from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string +from homekit.modem import E3372, ModemsConfig, MacroNetWorkType +from homekit.inverter.config import InverterdConfig +from homekit.relay.sunxi_h3_client import RelayClient +from homekit import http + + +class WebKbnConfig(AppConfigUnit): + NAME = 'web_kbn' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'listen_addr': cls._addr_schema(required=True), + 'assets_public_path': {'type': 'string'}, + 'pump_addr': cls._addr_schema(required=True), + 'inverter_grafana_url': {'type': 'string'}, + 'sensors_grafana_url': {'type': 'string'}, + } + + +STATIC_FILES = [ + 'bootstrap.min.css', + 'bootstrap.min.js', + 'polyfills.js', + 'app.js', + 'app.css' +] + + +def get_js_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'<script src="{config.app_config["assets_public_path"]}/{file}" type="text/javascript"></script>' + + +def get_css_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">' + + +def get_head_static() -> str: + buf = StringIO() + for file in STATIC_FILES: + v = 2 + try: + q_ind = file.index('?') + v = file[q_ind+1:] + file = file[:file.index('?')] + except ValueError: + pass + + if file.endswith('.js'): + buf.write(get_js_link(file, v)) + else: + buf.write(get_css_link(file, v)) + return buf.getvalue() + + +def get_modem_client(modem_cfg: dict) -> E3372: + return E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth']) + + +def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: + cl = get_modem_client(modem_cfg) + + signal = cl.device_signal + status = cl.monitoring_status + traffic = cl.traffic_stats + + if get_raw: + device_info = cl.device_information + dialup_conn = cl.dialup_connection + return signal, status, traffic, device_info, dialup_conn + else: + network_type_label = re.sub('^MACRO_NET_WORK_TYPE(_EX)?_', '', MacroNetWorkType(int(status['CurrentNetworkType'])).name) + return { + 'type': network_type_label, + 'level': int(status['SignalIcon']) if 'SignalIcon' in status else 0, + 'rssi': signal['rssi'], + 'sinr': signal['sinr'], + 'connected_time': seconds_to_human_readable_string(int(traffic['CurrentConnectTime'])), + 'downloaded': filesize_fmt(int(traffic['CurrentDownload'])), + 'uploaded': filesize_fmt(int(traffic['CurrentUpload'])) + } + + +def get_pump_client() -> RelayClient: + addr = config.app_config['pump_addr'] + cl = RelayClient(host=addr.host, port=addr.port) + cl.connect() + return cl + + +def get_inverter_client() -> inverterd.Client: + cl = inverterd.Client(host=InverterdConfig()['remote_addr'].host) + cl.connect() + cl.format(inverterd.Format.JSON) + return cl + + +def get_inverter_data() -> tuple: + cl = get_inverter_client() + + status = json.loads(cl.exec('get-status'))['data'] + rated = json.loads(cl.exec('get-rated'))['data'] + + power_direction = status['battery_power_direction'].lower() + power_direction = re.sub('ge$', 'ging', power_direction) + + charging_rate = '' + if power_direction == 'charging': + charging_rate = ' @ %s %s' % ( + status['battery_charge_current']['value'], + status['battery_charge_current']['unit']) + elif power_direction == 'discharging': + charging_rate = ' @ %s %s' % ( + status['battery_discharge_current']['value'], + status['battery_discharge_current']['unit']) + + html = '<b>Battery:</b> %s %s' % ( + status['battery_voltage']['value'], + status['battery_voltage']['unit']) + html += ' (%s%s, ' % ( + status['battery_capacity']['value'], + status['battery_capacity']['unit']) + html += '%s%s)' % (power_direction, charging_rate) + + html += "\n" + html += '<b>Load:</b> %s %s' % ( + status['ac_output_active_power']['value'], + status['ac_output_active_power']['unit']) + html += ' (%s%%)' % (status['output_load_percent']['value'],) + + if status['pv1_input_power']['value'] > 0: + html += "\n" + html += '<b>Input power:</b> %s %s' % ( + status['pv1_input_power']['value'], + status['pv1_input_power']['unit']) + + if status['grid_voltage']['value'] > 0 or status['grid_freq']['value'] > 0: + html += "\n" + html += '<b>AC input:</b> %s %s' % ( + status['grid_voltage']['value'], + status['grid_voltage']['unit']) + html += ', %s %s' % ( + status['grid_freq']['value'], + status['grid_freq']['unit']) + + html += "\n" + html += '<b>Priority:</b> %s' % (rated['output_source_priority'],) + + html = html.replace("\n", '<br>') + + return status, rated, html + + +class WebSite(http.HTTPServer): + _modems_config: ModemsConfig + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._modems_config = ModemsConfig() + + aiohttp_jinja2.setup( + self.app, + loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')), + autoescape=jinja2.select_autoescape(['html', 'xml']), + ) + env = aiohttp_jinja2.get_env(self.app) + env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':')) + + self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) + + self.get('/main.cgi', self.index) + + self.get('/modems.cgi', self.modems) + self.get('/modems/info.ajx', self.modems_ajx) + self.get('/modems/verbose.cgi', self.modems_verbose) + + self.get('/inverter.cgi', self.inverter) + self.get('/inverter.ajx', self.inverter_ajx) + self.get('/pump.cgi', self.pump) + self.get('/sms.cgi', self.sms) + self.post('/sms.cgi', self.sms_post) + + async def render_page(self, + req: http.Request, + template_name: str, + title: Optional[str] = None, + context: Optional[dict] = None): + if context is None: + context = {} + context = { + **context, + 'head_static': get_head_static() + } + if title is not None: + context['title'] = title + response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context) + return response + + async def index(self, req: http.Request): + ctx = {} + for k in 'inverter', 'sensors': + ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url'] + return await self.render_page(req, 'index', + title="Home web site", + context=ctx) + + async def modems(self, req: http.Request): + return await self.render_page(req, 'modems', + title='Состояние модемов', + context=dict(modems=self._modems_config)) + + async def modems_ajx(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.keys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg)) + + html = aiohttp_jinja2.render_string('modem_data.j2', req, context=dict( + modem_data=modem_data, + modem=modem + )) + + return self.ok({'html': html}) + + async def modems_verbose(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.keys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True)) + data = [ + ['Signal', signal], + ['Connection', status], + ['Traffic', traffic], + ['Device info', device], + ['Dialup connection', dialup_conn] + ] + + modem_name = self._modems_config.getfullname(modem) + return await self.render_page(req, 'modem_verbose', + title=f'Подробная информация о модеме "{modem_name}"', + context=dict(data=data, modem_name=modem_name)) + + async def sms(self, req: http.Request): + modem = req.query.get('id', list(self._modems_config.keys())[0]) + is_outbox = int(req.query.get('outbox', 0)) == 1 + error = req.query.get('error', None) + sent = int(req.query.get('sent', 0)) == 1 + + cl = get_modem_client(self._modems_config[modem]) + messages = cl.sms_list(1, 20, is_outbox) + return await self.render_page(req, 'sms', + title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})", + context=dict( + modems=self._modems_config, + selected_modem=modem, + is_outbox=is_outbox, + error=error, + is_sent=sent, + messages=messages + )) + + async def sms_post(self, req: http.Request): + modem = req.query.get('id', list(self._modems_config.keys())[0]) + is_outbox = int(req.query.get('outbox', 0)) == 1 + + fd = await req.post() + phone = fd.get('phone', None) + text = fd.get('text', None) + + return_url = f'/sms.cgi?id={modem}&outbox={int(is_outbox)}' + phone = re.sub('\s+', '', phone) + + if len(phone) > 4: + country = None + if not phone.startswith('+'): + country = 'RU' + number = phonenumbers.parse(phone, country) + if not phonenumbers.is_valid_number(number): + raise HTTPFound(f'{return_url}&error=Неверный+номер') + phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164) + + cl = get_modem_client(self._modems_config[modem]) + cl.sms_send(phone, text) + raise HTTPFound(return_url) + + async def inverter(self, req: http.Request): + action = req.query.get('do', None) + if action == 'set-osp': + val = req.query.get('value') + if val not in ('sub', 'sbu'): + raise ValueError('invalid osp value') + cl = get_inverter_client() + cl.exec('set-output-source-priority', + arguments=(val.upper(),)) + raise HTTPFound('/inverter.cgi') + + status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data) + return await self.render_page(req, 'inverter', + title='Инвертор', + context=dict(status=status, rated=rated, html=html)) + + async def inverter_ajx(self, req: http.Request): + status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data) + return self.ok({'html': html}) + + async def pump(self, req: http.Request): + # TODO + # these are blocking calls + # should be rewritten using aio + + cl = get_pump_client() + + action = req.query.get('set', None) + if action in ('on', 'off'): + getattr(cl, action)() + raise HTTPFound('/pump.cgi') + + status = cl.status() + return await self.render_page(req, 'pump', + title='Насос', + context=dict(status=status)) + + +if __name__ == '__main__': + config.load_app(WebKbnConfig) + + server = WebSite(config.app_config['listen_addr']) + server.run() diff --git a/doc/openwrt_logger.md b/doc/openwrt_logger.md new file mode 100644 index 0000000..1179c8b --- /dev/null +++ b/doc/openwrt_logger.md @@ -0,0 +1,28 @@ +# openwrt_logger.py + +This script is supposed to be run by cron every 5 minutes or so. +It looks for new lines in log file and sends them to remote server. + +OpenWRT must have remote logging enabled (UDP; IP of host this script is launched on; port 514) + +`/etc/rsyslog.conf` contains following (assuming `192.168.1.1` is the router IP): + +``` +$ModLoad imudp +$UDPServerRun 514 +:fromhost-ip, isequal, "192.168.1.1" /var/log/openwrt.log +& ~ +``` + +Also comment out the following line: +``` +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat +``` + +Cron line example: +``` +* * * * * /home/user/homekit/src/openwrt_logger.py --access-point 1 --file /var/wrtlogfs/openwrt-5.log >/dev/null +``` + +`/var/wrtlogfs` is recommended to be tmpfs, to avoid writes on mmc card, in case +you use arm sbcs as I do.
\ No newline at end of file diff --git a/tools/lib.bash b/include/bash/include.bash index 1d73ab2..1d73ab2 100644 --- a/tools/lib.bash +++ b/include/bash/include.bash diff --git a/platformio/common/include/homekit/logging.h b/include/pio/include/homekit/logging.h index 559ca33..559ca33 100644 --- a/platformio/common/include/homekit/logging.h +++ b/include/pio/include/homekit/logging.h diff --git a/platformio/common/include/homekit/macros.h b/include/pio/include/homekit/macros.h index 7d3ad83..7d3ad83 100644 --- a/platformio/common/include/homekit/macros.h +++ b/include/pio/include/homekit/macros.h diff --git a/platformio/common/include/homekit/stopwatch.h b/include/pio/include/homekit/stopwatch.h index bac2fcc..bac2fcc 100644 --- a/platformio/common/include/homekit/stopwatch.h +++ b/include/pio/include/homekit/stopwatch.h diff --git a/platformio/common/include/homekit/util.h b/include/pio/include/homekit/util.h index e0780d8..e0780d8 100644 --- a/platformio/common/include/homekit/util.h +++ b/include/pio/include/homekit/util.h diff --git a/platformio/common/libs/config/homekit/config.cpp b/include/pio/libs/config/homekit/config.cpp index 5bafcad..5bafcad 100644 --- a/platformio/common/libs/config/homekit/config.cpp +++ b/include/pio/libs/config/homekit/config.cpp diff --git a/platformio/common/libs/config/homekit/config.h b/include/pio/libs/config/homekit/config.h index 28f01fb..28f01fb 100644 --- a/platformio/common/libs/config/homekit/config.h +++ b/include/pio/libs/config/homekit/config.h diff --git a/platformio/common/libs/config/library.json b/include/pio/libs/config/library.json index 720d093..720d093 100644 --- a/platformio/common/libs/config/library.json +++ b/include/pio/libs/config/library.json diff --git a/platformio/common/libs/http_server/homekit/http_server.cpp b/include/pio/libs/http_server/homekit/http_server.cpp index ea81f5b..ea81f5b 100644 --- a/platformio/common/libs/http_server/homekit/http_server.cpp +++ b/include/pio/libs/http_server/homekit/http_server.cpp diff --git a/platformio/common/libs/http_server/homekit/http_server.h b/include/pio/libs/http_server/homekit/http_server.h index 8725a88..8725a88 100644 --- a/platformio/common/libs/http_server/homekit/http_server.h +++ b/include/pio/libs/http_server/homekit/http_server.h diff --git a/platformio/common/libs/http_server/library.json b/include/pio/libs/http_server/library.json index ee2d369..ee2d369 100644 --- a/platformio/common/libs/http_server/library.json +++ b/include/pio/libs/http_server/library.json diff --git a/platformio/common/libs/led/homekit/led.cpp b/include/pio/libs/led/homekit/led.cpp index ffefb04..ffefb04 100644 --- a/platformio/common/libs/led/homekit/led.cpp +++ b/include/pio/libs/led/homekit/led.cpp diff --git a/platformio/common/libs/led/homekit/led.h b/include/pio/libs/led/homekit/led.h index 775d2eb..775d2eb 100644 --- a/platformio/common/libs/led/homekit/led.h +++ b/include/pio/libs/led/homekit/led.h diff --git a/platformio/common/libs/led/library.json b/include/pio/libs/led/library.json index 6785d42..6785d42 100644 --- a/platformio/common/libs/led/library.json +++ b/include/pio/libs/led/library.json diff --git a/platformio/common/libs/main/homekit/main.cpp b/include/pio/libs/main/homekit/main.cpp index 816c764..816c764 100644 --- a/platformio/common/libs/main/homekit/main.cpp +++ b/include/pio/libs/main/homekit/main.cpp diff --git a/platformio/common/libs/main/homekit/main.h b/include/pio/libs/main/homekit/main.h index 78a0695..78a0695 100644 --- a/platformio/common/libs/main/homekit/main.h +++ b/include/pio/libs/main/homekit/main.h diff --git a/include/pio/libs/main/library.json b/include/pio/libs/main/library.json new file mode 100644 index 0000000..c5586d8 --- /dev/null +++ b/include/pio/libs/main/library.json @@ -0,0 +1,12 @@ +{ + "name": "homekit_main", + "version": "1.0.11", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt_module_ota": "file://../../include/pio/libs/mqtt_module_ota", + "homekit_mqtt_module_diagnostics": "file://../../include/pio/libs/mqtt_module_diagnostics" + } +} + diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp b/include/pio/libs/mqtt/homekit/mqtt/module.cpp index 0ac7637..0ac7637 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp +++ b/include/pio/libs/mqtt/homekit/mqtt/module.cpp diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.h b/include/pio/libs/mqtt/homekit/mqtt/module.h index 0a328f3..0a328f3 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/module.h +++ b/include/pio/libs/mqtt/homekit/mqtt/module.h diff --git a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp index aa769a5..83764ca 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp +++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp @@ -119,7 +119,7 @@ void Mqtt::reconnect() { void Mqtt::disconnect() { // TODO test how this works??? reconnectTimer.detach(); - client.disconnect(); + client.disconnect(true); } void Mqtt::loop() { diff --git a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.h b/include/pio/libs/mqtt/homekit/mqtt/mqtt.h index 9e0c2be..9e0c2be 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.h +++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.h diff --git a/platformio/common/libs/mqtt/homekit/mqtt/payload.h b/include/pio/libs/mqtt/homekit/mqtt/payload.h index 3e0fe0c..3e0fe0c 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/payload.h +++ b/include/pio/libs/mqtt/homekit/mqtt/payload.h diff --git a/platformio/common/libs/mqtt/library.json b/include/pio/libs/mqtt/library.json index f3f2504..6238c21 100644 --- a/platformio/common/libs/mqtt/library.json +++ b/include/pio/libs/mqtt/library.json @@ -1,6 +1,6 @@ { "name": "homekit_mqtt", - "version": "1.0.11", + "version": "1.0.12", "build": { "flags": "-I../../include" } diff --git a/platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp index e0f797e..e0f797e 100644 --- a/platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp +++ b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp diff --git a/platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h index bb7a81a..bb7a81a 100644 --- a/platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h +++ b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h diff --git a/platformio/common/libs/mqtt_module_diagnostics/library.json b/include/pio/libs/mqtt_module_diagnostics/library.json index a3d3244..70acb79 100644 --- a/platformio/common/libs/mqtt_module_diagnostics/library.json +++ b/include/pio/libs/mqtt_module_diagnostics/library.json @@ -1,10 +1,10 @@ { "name": "homekit_mqtt_module_diagnostics", - "version": "1.0.2", + "version": "1.0.3", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt" + "homekit_mqtt": "file://../../include/pio/libs/mqtt" } } diff --git a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp index 4e976cd..4e976cd 100644 --- a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp +++ b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp diff --git a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.h b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h index df4f7ce..df4f7ce 100644 --- a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.h +++ b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h diff --git a/include/pio/libs/mqtt_module_ota/library.json b/include/pio/libs/mqtt_module_ota/library.json new file mode 100644 index 0000000..1577fed --- /dev/null +++ b/include/pio/libs/mqtt_module_ota/library.json @@ -0,0 +1,11 @@ +{ + "name": "homekit_mqtt_module_ota", + "version": "1.0.6", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_led": "file://../../include/pio/libs/led", + "homekit_mqtt": "file://../../include/pio/libs/mqtt" + } +} diff --git a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp index 90c57f9..90c57f9 100644 --- a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp +++ b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp diff --git a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h index e245527..e245527 100644 --- a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h +++ b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h diff --git a/include/pio/libs/mqtt_module_relay/library.json b/include/pio/libs/mqtt_module_relay/library.json new file mode 100644 index 0000000..18a510c --- /dev/null +++ b/include/pio/libs/mqtt_module_relay/library.json @@ -0,0 +1,11 @@ +{ + "name": "homekit_mqtt_module_relay", + "version": "1.0.6", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_relay": "file://../../include/pio/libs/relay" + } +} diff --git a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp index 409f38f..409f38f 100644 --- a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp +++ b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp diff --git a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h index 7b28afc..7b28afc 100644 --- a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h +++ b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h diff --git a/platformio/common/libs/mqtt_module_temphum/library.json b/include/pio/libs/mqtt_module_temphum/library.json index 068debd..c7ee7af 100644 --- a/platformio/common/libs/mqtt_module_temphum/library.json +++ b/include/pio/libs/mqtt_module_temphum/library.json @@ -5,7 +5,7 @@ "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt", - "homekit_temphum": "file://../common/libs/temphum" + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_temphum": "file://../../include/pio/libs/temphum" } } diff --git a/platformio/common/libs/relay/homekit/relay.cpp b/include/pio/libs/relay/homekit/relay.cpp index b00a7a2..b00a7a2 100644 --- a/platformio/common/libs/relay/homekit/relay.cpp +++ b/include/pio/libs/relay/homekit/relay.cpp diff --git a/platformio/common/libs/relay/homekit/relay.h b/include/pio/libs/relay/homekit/relay.h index 288cc05..288cc05 100644 --- a/platformio/common/libs/relay/homekit/relay.h +++ b/include/pio/libs/relay/homekit/relay.h diff --git a/platformio/common/libs/relay/library.json b/include/pio/libs/relay/library.json index e878248..e878248 100644 --- a/platformio/common/libs/relay/library.json +++ b/include/pio/libs/relay/library.json diff --git a/platformio/common/libs/static/homekit/static.cpp b/include/pio/libs/static/homekit/static.cpp index 366a09f..366a09f 100644 --- a/platformio/common/libs/static/homekit/static.cpp +++ b/include/pio/libs/static/homekit/static.cpp diff --git a/platformio/common/libs/static/homekit/static.h b/include/pio/libs/static/homekit/static.h index c2617e9..c2617e9 100644 --- a/platformio/common/libs/static/homekit/static.h +++ b/include/pio/libs/static/homekit/static.h diff --git a/platformio/common/libs/static/library.json b/include/pio/libs/static/library.json index bc650d7..bc650d7 100644 --- a/platformio/common/libs/static/library.json +++ b/include/pio/libs/static/library.json diff --git a/platformio/common/libs/temphum/homekit/temphum.cpp b/include/pio/libs/temphum/homekit/temphum.cpp index e69b3a5..e69b3a5 100644 --- a/platformio/common/libs/temphum/homekit/temphum.cpp +++ b/include/pio/libs/temphum/homekit/temphum.cpp diff --git a/platformio/common/libs/temphum/homekit/temphum.h b/include/pio/libs/temphum/homekit/temphum.h index 1952ce0..1952ce0 100644 --- a/platformio/common/libs/temphum/homekit/temphum.h +++ b/include/pio/libs/temphum/homekit/temphum.h diff --git a/platformio/common/libs/temphum/library.json b/include/pio/libs/temphum/library.json index 329b7ca..4cf5c63 100644 --- a/platformio/common/libs/temphum/library.json +++ b/include/pio/libs/temphum/library.json @@ -1,6 +1,6 @@ { "name": "homekit_temphum", - "version": "1.0.3", + "version": "1.0.4", "build": { "flags": "-I../../include" } diff --git a/platformio/common/libs/wifi/homekit/wifi.cpp b/include/pio/libs/wifi/homekit/wifi.cpp index 3060dd6..3060dd6 100644 --- a/platformio/common/libs/wifi/homekit/wifi.cpp +++ b/include/pio/libs/wifi/homekit/wifi.cpp diff --git a/platformio/common/libs/wifi/homekit/wifi.h b/include/pio/libs/wifi/homekit/wifi.h index 3fe77cb..3fe77cb 100644 --- a/platformio/common/libs/wifi/homekit/wifi.h +++ b/include/pio/libs/wifi/homekit/wifi.h diff --git a/platformio/common/libs/wifi/library.json b/include/pio/libs/wifi/library.json index c7faecd..c7faecd 100644 --- a/platformio/common/libs/wifi/library.json +++ b/include/pio/libs/wifi/library.json diff --git a/platformio/common/make_static.sh b/include/pio/make_static.sh index d207e57..d207e57 100755 --- a/platformio/common/make_static.sh +++ b/include/pio/make_static.sh diff --git a/platformio/common/static/app.js b/include/pio/static/app.js index 299230c..299230c 100644 --- a/platformio/common/static/app.js +++ b/include/pio/static/app.js diff --git a/platformio/common/static/favicon.ico b/include/pio/static/favicon.ico Binary files differindex 6940e4f..6940e4f 100644 --- a/platformio/common/static/favicon.ico +++ b/include/pio/static/favicon.ico diff --git a/platformio/common/static/index.html b/include/pio/static/index.html index d4a8040..d4a8040 100644 --- a/platformio/common/static/index.html +++ b/include/pio/static/index.html diff --git a/platformio/common/static/md5.js b/include/pio/static/md5.js index b707a4e..b707a4e 100644 --- a/platformio/common/static/md5.js +++ b/include/pio/static/md5.js diff --git a/platformio/common/static/style.css b/include/pio/static/style.css index 32bd02c..32bd02c 100644 --- a/platformio/common/static/style.css +++ b/include/pio/static/style.css diff --git a/src/__init__.py b/include/py/__init__.py index e69de29..e69de29 100644 --- a/src/__init__.py +++ b/include/py/__init__.py diff --git a/src/home/__init__.py b/include/py/homekit/__init__.py index e69de29..e69de29 100644 --- a/src/home/__init__.py +++ b/include/py/homekit/__init__.py diff --git a/include/py/homekit/api/__init__.py b/include/py/homekit/api/__init__.py new file mode 100644 index 0000000..d641f62 --- /dev/null +++ b/include/py/homekit/api/__init__.py @@ -0,0 +1,19 @@ +import importlib + +__all__ = [ + # web_api_client.py + 'WebApiClient', + 'RequestParams', + + # config.py + 'WebApiConfig' +] + + +def __getattr__(name): + if name in __all__: + file = 'config' if name == 'WebApiConfig' else 'web_api_client' + module = importlib.import_module(f'.{file}', __name__) + return getattr(module, name) + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/include/py/homekit/api/__init__.pyi b/include/py/homekit/api/__init__.pyi new file mode 100644 index 0000000..5b98161 --- /dev/null +++ b/include/py/homekit/api/__init__.pyi @@ -0,0 +1,5 @@ +from .web_api_client import ( + RequestParams as RequestParams, + WebApiClient as WebApiClient +) +from .config import WebApiConfig as WebApiConfig diff --git a/include/py/homekit/api/config.py b/include/py/homekit/api/config.py new file mode 100644 index 0000000..00c1097 --- /dev/null +++ b/include/py/homekit/api/config.py @@ -0,0 +1,15 @@ +from ..config import ConfigUnit +from typing import Optional, Union + + +class WebApiConfig(ConfigUnit): + NAME = 'web_api' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'listen_addr': cls._addr_schema(required=True), + 'host': cls._addr_schema(required=True), + 'token': dict(type='string', required=True), + 'recordings_dir': dict(type='string', required=True) + }
\ No newline at end of file diff --git a/src/home/api/errors/__init__.py b/include/py/homekit/api/errors/__init__.py index efb06aa..efb06aa 100644 --- a/src/home/api/errors/__init__.py +++ b/include/py/homekit/api/errors/__init__.py diff --git a/src/home/api/errors/api_response_error.py b/include/py/homekit/api/errors/api_response_error.py index 85d788b..85d788b 100644 --- a/src/home/api/errors/api_response_error.py +++ b/include/py/homekit/api/errors/api_response_error.py diff --git a/src/home/api/types/__init__.py b/include/py/homekit/api/types/__init__.py index 9f27ff6..22ce4e6 100644 --- a/src/home/api/types/__init__.py +++ b/include/py/homekit/api/types/__init__.py @@ -1,5 +1,4 @@ from .types import ( - BotType, TemperatureSensorDataType, TemperatureSensorLocation, SoundSensorLocation diff --git a/src/home/api/types/types.py b/include/py/homekit/api/types/types.py index 981e798..294a712 100644 --- a/src/home/api/types/types.py +++ b/include/py/homekit/api/types/types.py @@ -1,17 +1,6 @@ from enum import Enum, auto -class BotType(Enum): - INVERTER = auto() - PUMP = auto() - SENSORS = auto() - ADMIN = auto() - SOUND = auto() - POLARIS_KETTLE = auto() - PUMP_MQTT = auto() - RELAY_MQTT = auto() - - class TemperatureSensorLocation(Enum): BIG_HOUSE_1 = auto() BIG_HOUSE_2 = auto() diff --git a/src/home/api/web_api_client.py b/include/py/homekit/api/web_api_client.py index 6677182..f9a8963 100644 --- a/src/home/api/web_api_client.py +++ b/include/py/homekit/api/web_api_client.py @@ -9,13 +9,15 @@ from enum import Enum, auto from typing import Optional, Callable, Union, List, Tuple, Dict from requests.auth import HTTPBasicAuth +from .config import WebApiConfig from .errors import ApiResponseError from .types import * from ..config import config from ..util import stringify from ..media import RecordFile, MediaNodeClient -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) +_config = WebApiConfig() RequestParams = namedtuple('RequestParams', 'params, files, method') @@ -26,7 +28,7 @@ class HTTPMethod(Enum): POST = auto() -class WebAPIClient: +class WebApiClient: token: str timeout: Union[float, Tuple[float, float]] basic_auth: Optional[HTTPBasicAuth] @@ -35,36 +37,26 @@ class WebAPIClient: async_success_handler: Optional[Callable] def __init__(self, timeout: Union[float, Tuple[float, float]] = 5): - self.token = config['api']['token'] + self.token = config['token'] self.timeout = timeout self.basic_auth = None self.do_async = False self.async_error_handler = None self.async_success_handler = None - if 'basic_auth' in config['api']: - ba = config['api']['basic_auth'] - col = ba.index(':') - - user = ba[:col] - pw = ba[col+1:] - - logger.debug(f'enabling basic auth: {user}:{pw}') - self.basic_auth = HTTPBasicAuth(user, pw) + # if 'basic_auth' in config['api']: + # ba = config['api']['basic_auth'] + # col = ba.index(':') + # + # user = ba[:col] + # pw = ba[col+1:] + # + # _logger.debug(f'enabling basic auth: {user}:{pw}') + # self.basic_auth = HTTPBasicAuth(user, pw) # api methods # ----------- - def log_bot_request(self, - bot: BotType, - user_id: int, - message: str): - return self._post('log/bot_request/', { - 'bot': bot.value, - 'user_id': str(user_id), - 'message': message - }) - def log_openwrt(self, lines: List[Tuple[int, str]], access_point: int): @@ -152,7 +144,7 @@ class WebAPIClient: params: dict, method: HTTPMethod = HTTPMethod.GET, files: Optional[Dict[str, str]] = None) -> Optional[any]: - domain = config['api']['host'] + domain = config['host'] kwargs = {} if self.basic_auth is not None: @@ -196,7 +188,7 @@ class WebAPIClient: try: f.close() except Exception as exc: - logger.exception(exc) + _logger.exception(exc) pass def _make_request_in_thread(self, name, params, method, files): @@ -204,7 +196,7 @@ class WebAPIClient: result = self._make_request(name, params, method, files) self._report_async_success(result, name, RequestParams(params=params, method=method, files=files)) except Exception as e: - logger.exception(e) + _logger.exception(e) self._report_async_error(e, name, RequestParams(params=params, method=method, files=files)) def enable_async(self, diff --git a/src/home/audio/__init__.py b/include/py/homekit/audio/__init__.py index e69de29..e69de29 100644 --- a/src/home/audio/__init__.py +++ b/include/py/homekit/audio/__init__.py diff --git a/src/home/audio/amixer.py b/include/py/homekit/audio/amixer.py index 5133c97..827e102 100644 --- a/src/home/audio/amixer.py +++ b/include/py/homekit/audio/amixer.py @@ -10,14 +10,14 @@ _default_step = 5 def has_control(s: str) -> bool: - for control in config['amixer']['controls']: + for control in config.app_config['amixer']['controls']: if control['name'] == s: return True return False def get_caps(s: str) -> List[str]: - for control in config['amixer']['controls']: + for control in config.app_config['amixer']['controls']: if control['name'] == s: return control['caps'] raise KeyError(f'control {s} not found') @@ -25,7 +25,7 @@ def get_caps(s: str) -> List[str]: def get_all() -> list: controls = [] - for control in config['amixer']['controls']: + for control in config.app_config['amixer']['controls']: controls.append({ 'name': control['name'], 'info': get(control['name']), @@ -55,8 +55,8 @@ def nocap(control): def _get_default_step() -> int: - if 'step' in config['amixer']: - return int(config['amixer']['step']) + if 'step' in config.app_config['amixer']: + return int(config.app_config['amixer']['step']) return _default_step @@ -75,7 +75,7 @@ def decr(control, step=None): def call(*args, return_code=False) -> Union[int, str]: with _lock: - result = subprocess.run([config['amixer']['bin'], *args], + result = subprocess.run([config.app_config['amixer']['bin'], *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if return_code: diff --git a/include/py/homekit/camera/__init__.py b/include/py/homekit/camera/__init__.py new file mode 100644 index 0000000..4875031 --- /dev/null +++ b/include/py/homekit/camera/__init__.py @@ -0,0 +1,2 @@ +from .types import CameraType, VideoContainerType, VideoCodecType, CaptureType +from .config import IpcamConfig
\ No newline at end of file diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py new file mode 100644 index 0000000..8aeb392 --- /dev/null +++ b/include/py/homekit/camera/config.py @@ -0,0 +1,141 @@ +import socket + +from ..config import ConfigUnit, LinuxBoardsConfig +from typing import Optional +from .types import CameraType, VideoContainerType, VideoCodecType + +_lbc = LinuxBoardsConfig() + + +def _validate_roi_line(field, value, error) -> bool: + p = value.split(' ') + if len(p) != 4: + error(field, f'{field}: must contain four coordinates separated by space') + for n in p: + if not n.isnumeric(): + error(field, f'{field}: invalid coordinates (not a number)') + return True + + +class IpcamConfig(ConfigUnit): + NAME = 'ipcam' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'cameras': { + 'type': 'dict', + 'keysrules': {'type': ['string', 'integer']}, + 'valuesrules': { + 'type': 'dict', + 'schema': { + 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True}, + 'motion': { + 'type': 'dict', + 'schema': { + 'threshold': {'type': ['float', 'integer']}, + 'roi': { + 'type': 'list', + 'schema': {'type': 'string', 'check_with': _validate_roi_line} + } + } + }, + } + } + }, + 'areas': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': { + 'type': 'list', + 'schema': {'type': ['string', 'integer']} # same type as for 'cameras' keysrules + } + }, + 'camera_ip_template': {'type': 'string', 'required': True}, + 'motion_padding': {'type': 'integer', 'required': True}, + 'motion_telegram': {'type': 'boolean', 'required': True}, + 'fix_interval': {'type': 'integer', 'required': True}, + 'fix_enabled': {'type': 'boolean', 'required': True}, + 'cleanup_min_gb': {'type': 'integer', 'required': True}, + 'cleanup_interval': {'type': 'integer', 'required': True}, + + # TODO FIXME + 'fragment_url_templates': cls._url_templates_schema(), + 'original_file_url_templates': cls._url_templates_schema(), + + 'hls_path': {'type': 'string', 'required': True}, + 'motion_processing_tmpfs_path': {'type': 'string', 'required': True}, + + 'rtsp_creds': { + 'required': True, + 'type': 'dict', + 'schema': { + 'login': {'type': 'string', 'required': True}, + 'password': {'type': 'string', 'required': True}, + } + }, + + 'web_creds': { + 'required': True, + 'type': 'dict', + 'schema': { + 'login': {'type': 'string', 'required': True}, + 'password': {'type': 'string', 'required': True}, + } + } + } + + @staticmethod + def custom_validator(data): + for n, cam in data['cams'].items(): + linux_box = _lbc[cam['server']] + if 'ext_hdd' not in linux_box: + raise ValueError(f'cam-{n}: linux box {cam["server"]} must have ext_hdd defined') + disk = cam['disk']-1 + if disk < 0 or disk >= len(linux_box['ext_hdd']): + raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') + + @classmethod + def _url_templates_schema(cls) -> dict: + return { + 'type': 'list', + 'empty': False, + 'schema': { + 'type': 'list', + 'empty': False, + 'schema': {'type': 'string'} + } + } + + # FIXME + def get_all_cam_names(self, + filter_by_server: Optional[str] = None, + filter_by_disk: Optional[int] = None) -> list[int]: + cams = [] + if filter_by_server is not None and filter_by_server not in _lbc: + raise ValueError(f'invalid filter_by_server: {filter_by_server} not found in {_lbc.__class__.__name__}') + for cam, params in self['cams'].items(): + if filter_by_server is None or params['server'] == filter_by_server: + if filter_by_disk is None or params['disk'] == filter_by_disk: + cams.append(int(cam)) + return cams + + # def get_all_cam_names_for_this_server(self, + # filter_by_disk: Optional[int] = None): + # return self.get_all_cam_names(filter_by_server=socket.gethostname(), + # filter_by_disk=filter_by_disk) + + # def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: + # return self['cams'][cam]['server'], self['cams'][cam]['disk'] + + def get_camera_container(self, camera: int) -> VideoContainerType: + return self.get_camera_type(camera).get_container() + + def get_camera_type(self, camera: int) -> CameraType: + return CameraType(self['cams'][camera]['type']) + + def get_rtsp_creds(self) -> tuple[str, str]: + return self['rtsp_creds']['login'], self['rtsp_creds']['password'] + + def get_camera_ip(self, camera: int) -> str: + return self['camera_ip_template'] % (str(camera),) diff --git a/src/home/camera/esp32.py b/include/py/homekit/camera/esp32.py index fe6de0e..fe6de0e 100644 --- a/src/home/camera/esp32.py +++ b/include/py/homekit/camera/esp32.py diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py new file mode 100644 index 0000000..da0fcc6 --- /dev/null +++ b/include/py/homekit/camera/types.py @@ -0,0 +1,58 @@ +from enum import Enum + + +class VideoContainerType(Enum): + MP4 = 'mp4' + MOV = 'mov' + + +class VideoCodecType(Enum): + H264 = 'h264' + H265 = 'h265' + + +class CameraType(Enum): + ESP32 = 'esp32' + ALIEXPRESS_NONAME = 'ali' + HIKVISION_264 = 'hik_264' + HIKVISION_265 = 'hik_265' + + def get_channel_url(self, channel: int) -> str: + if channel not in (1, 2): + raise ValueError(f'channel {channel} is invalid') + if channel == 1: + return '' + elif channel == 2: + if self.value in (CameraType.HIKVISION_264, CameraType.HIKVISION_265): + return '/Streaming/Channels/2' + elif self.value == CameraType.ALIEXPRESS_NONAME: + return '/?stream=1.sdp' + else: + raise ValueError(f'unsupported camera type {self.value}') + + def get_codec(self, channel: int) -> VideoCodecType: + if channel == 1: + return VideoCodecType.H264 if self.value == CameraType.HIKVISION_264 else VideoCodecType.H265 + elif channel == 2: + return VideoCodecType.H265 if self.value == CameraType.ALIEXPRESS_NONAME else VideoCodecType.H264 + else: + raise ValueError(f'unexpected channel {channel}') + + def get_container(self) -> VideoContainerType: + return VideoContainerType.MP4 if self.get_codec(1) == VideoCodecType.H264 else VideoContainerType.MOV + + +class TimeFilterType(Enum): + FIX = 'fix' + MOTION = 'motion' + MOTION_START = 'motion_start' + + +class TelegramLinkType(Enum): + FRAGMENT = 'fragment' + ORIGINAL_FILE = 'original_file' + + +class CaptureType(Enum): + HLS = 'hls' + RECORD = 'record' diff --git a/src/home/camera/util.py b/include/py/homekit/camera/util.py index 97f35aa..58c2c70 100644 --- a/src/home/camera/util.py +++ b/include/py/homekit/camera/util.py @@ -2,13 +2,21 @@ import asyncio import os.path import logging import psutil +import re +from datetime import datetime from typing import List, Tuple from ..util import chunks -from ..config import config +from ..config import config, LinuxBoardsConfig +from .config import IpcamConfig +from .types import VideoContainerType _logger = logging.getLogger(__name__) -_temporary_fixing = '.temporary_fixing.mp4' +_ipcam_config = IpcamConfig() +_lbc_config = LinuxBoardsConfig() + +datetime_format = '%Y-%m-%d-%H.%M.%S' +datetime_format_re = r'\d{4}-\d{2}-\d{2}-\d{2}\.\d{2}.\d{2}' def _get_ffmpeg_path() -> str: @@ -26,7 +34,8 @@ def time2seconds(time: str) -> int: async def ffmpeg_recreate(filename: str): filedir = os.path.dirname(filename) - tempname = os.path.join(filedir, _temporary_fixing) + _, fileext = os.path.splitext(filename) + tempname = os.path.join(filedir, f'.temporary_fixing.{fileext}') mtime = os.path.getmtime(filename) args = [_get_ffmpeg_path(), '-nostats', '-loglevel', 'error', '-i', filename, '-c', 'copy', '-y', tempname] @@ -104,4 +113,57 @@ def has_handle(fpath): except Exception: pass - return False
\ No newline at end of file + return False + + +def get_recordings_path(cam: int) -> str: + server, disk = _ipcam_config.get_cam_server_and_disk(cam) + disks = _lbc_config.get_board_disks(server) + disk_mountpoint = disks[disk-1] + return f'{disk_mountpoint}/cam-{cam}' + + +def get_motion_path(cam: int) -> str: + return f'{get_recordings_path(cam)}/motion' + + +def is_valid_recording_name(filename: str) -> bool: + if not filename.startswith('record_'): + return False + + for container_type in VideoContainerType: + if filename.endswith(f'.{container_type.value}'): + return True + + return False + + +def datetime_from_filename(name: str) -> datetime: + name = os.path.basename(name) + exts = '|'.join([t.value for t in VideoContainerType]) + + if name.startswith('record_'): + return datetime.strptime(re.match(rf'record_(.*?)\.(?:{exts})', name).group(1), datetime_format) + + m = re.match(rf'({datetime_format_re})__{datetime_format_re}\.(?:{exts})', name) + if m: + return datetime.strptime(m.group(1), datetime_format) + + raise ValueError(f'unrecognized filename format: {name}') + + +def get_hls_channel_name(cam: int, channel: int) -> str: + name = str(cam) + if channel == 2: + name += '-low' + return name + + +def get_hls_directory(cam, channel) -> str: + dirname = os.path.join( + _ipcam_config['hls_path'], + get_hls_channel_name(cam, channel) + ) + if not os.path.exists(dirname): + os.makedirs(dirname) + return dirname
\ No newline at end of file diff --git a/src/home/config/__init__.py b/include/py/homekit/config/__init__.py index 1321047..8fedfa6 100644 --- a/src/home/config/__init__.py +++ b/include/py/homekit/config/__init__.py @@ -2,11 +2,11 @@ from .config import ( Config, ConfigUnit, AppConfigUnit, - TranslationsUnit, + Translation, config, is_development_mode, setup_logging, - app_config + CONFIG_DIRECTORIES ) from ._configs import ( LinuxBoardsConfig, diff --git a/src/home/config/_configs.py b/include/py/homekit/config/_configs.py index 3a1aae5..2cd2aca 100644 --- a/src/home/config/_configs.py +++ b/include/py/homekit/config/_configs.py @@ -5,8 +5,8 @@ from typing import Optional class ServicesListConfig(ConfigUnit): NAME = 'services_list' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'list', 'empty': False, @@ -19,13 +19,14 @@ class ServicesListConfig(ConfigUnit): class LinuxBoardsConfig(ConfigUnit): NAME = 'linux_boards' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'dict', 'schema': { 'mdns': {'type': 'string', 'required': True}, 'board': {'type': 'string', 'required': True}, + 'location': {'type': 'string', 'required': True}, 'network': { 'type': 'list', 'required': True, @@ -53,3 +54,9 @@ class LinuxBoardsConfig(ConfigUnit): }, } } + + def get_board_disks(self, name: str) -> list[dict]: + return self[name]['ext_hdd'] + + def get_board_disks_count(self, name: str) -> int: + return len(self[name]['ext_hdd']) diff --git a/src/home/config/config.py b/include/py/homekit/config/config.py index 26e28f8..fec92a6 100644 --- a/src/home/config/config.py +++ b/include/py/homekit/config/config.py @@ -1,23 +1,32 @@ import yaml import logging import os -import pprint +import cerberus +import cerberus.errors from abc import ABC -from cerberus import Validator, DocumentError from typing import Optional, Any, MutableMapping, Union from argparse import ArgumentParser from enum import Enum, auto from os.path import join, isdir, isfile -from ..util import parse_addr +from ..util import Addr +from pprint import pprint + + +class MyValidator(cerberus.Validator): + def _normalize_coerce_addr(self, value): + return Addr.fromstring(value) + + +MyValidator.types_mapping['addr'] = cerberus.TypeDefinition('Addr', (Addr,), ()) -SUPPORTED_LANGUAGES = ('en', 'ru') CONFIG_DIRECTORIES = ( join(os.environ['HOME'], '.config', 'homekit'), '/etc/homekit' ) + class RootSchemaType(Enum): DEFAULT = auto() DICT = auto() @@ -28,10 +37,13 @@ class BaseConfigUnit(ABC): _data: MutableMapping[str, Any] _logger: logging.Logger - def __init__(self, name=None): + def __init__(self): self._data = {} self._logger = logging.getLogger(self.__class__.__name__) + def __iter__(self): + return iter(self._data) + def __getitem__(self, key): return self._data[key] @@ -44,6 +56,8 @@ class BaseConfigUnit(ABC): def load_from(self, path: str): with open(path, 'r') as fd: self._data = yaml.safe_load(fd) + if self._data is None: + raise TypeError(f'config file {path} is empty') def get(self, key: Optional[str] = None, @@ -64,17 +78,37 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') + def values(self): + return self._data.values() + + def keys(self): + return self._data.keys() + + def items(self): + return self._data.items() + class ConfigUnit(BaseConfigUnit): NAME = 'dumb' - def __init__(self, name=None): + _instance = None + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls._instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(ConfigUnit, cls).__new__(cls, *args, **kwargs) + return cls._instance + + def __init__(self, name=None, load=True): super().__init__() self._data = {} self._logger = logging.getLogger(self.__class__.__name__) - if self.NAME != 'dumb': + if self.NAME != 'dumb' and load: self.load_from(self.get_config_path()) self.validate() @@ -94,12 +128,21 @@ class ConfigUnit(BaseConfigUnit): if isfile(filename): return filename - raise IOError(f'\'{name}\'.yaml not found') + raise IOError(f'\'{name}.yaml\' not found') - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return None + @classmethod + def _addr_schema(cls, required=False, only_ip=False, **kwargs): + return { + 'type': 'addr', + 'coerce': Addr.fromstring if not only_ip else Addr.fromipstring, + 'required': required, + **kwargs + } + def validate(self): schema = self.schema() if not schema: @@ -110,7 +153,7 @@ class ConfigUnit(BaseConfigUnit): schema['logging'] = { 'type': 'dict', 'schema': { - 'logging': {'type': 'bool'} + 'verbose': {'type': 'boolean'} } } @@ -120,40 +163,49 @@ class ConfigUnit(BaseConfigUnit): rst = RootSchemaType.DICT elif schema['type'] == 'list': rst = RootSchemaType.LIST + elif schema['roottype'] == 'dict': + del schema['roottype'] + rst = RootSchemaType.DICT except KeyError: pass + v = MyValidator() + need_document = False + if rst == RootSchemaType.DICT: - v = Validator({'document': { - 'type': 'dict', - 'keysrules': {'type': 'string'}, - 'valuesrules': schema - }}) - result = v.validate({'document': self._data}) + normalized = v.validated({'document': self._data}, + {'document': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': schema + }}) + need_document = True elif rst == RootSchemaType.LIST: - v = Validator({'document': schema}) - result = v.validate({'document': self._data}) + v = MyValidator() + normalized = v.validated({'document': self._data}, {'document': schema}) + need_document = True else: - v = Validator(schema) - result = v.validate(self._data) - # pprint.pprint(self._data) - if not result: - # pprint.pprint(v.errors) - raise DocumentError(f'{self.__class__.__name__}: failed to validate data:\n{pprint.pformat(v.errors)}') + normalized = v.validated(self._data, schema) + + if not normalized: + raise cerberus.DocumentError(f'validation failed: {v.errors}') + + if need_document: + normalized = normalized['document'] + + self._data = normalized + try: self.custom_validator(self._data) except Exception as e: - raise DocumentError(f'{self.__class__.__name__}: {str(e)}') + raise cerberus.DocumentError(f'{self.__class__.__name__}: {str(e)}') @staticmethod def custom_validator(data): pass def get_addr(self, key: str): - return parse_addr(self.get(key)) - - def items(self): - return self._data.items() + return Addr.fromstring(self.get(key)) class AppConfigUnit(ConfigUnit): @@ -162,7 +214,7 @@ class AppConfigUnit(ConfigUnit): _logging_file: Optional[str] def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super().__init__(load=False, *args, **kwargs) self._logging_verbose = False self._logging_fmt = None self._logging_file = None @@ -173,7 +225,7 @@ class AppConfigUnit(ConfigUnit): def logging_get_fmt(self) -> Optional[str]: try: return self['logging']['default_fmt'] - except KeyError: + except (KeyError, TypeError): return self._logging_fmt def logging_set_file(self, file: str) -> None: @@ -182,7 +234,7 @@ class AppConfigUnit(ConfigUnit): def logging_get_file(self) -> Optional[str]: try: return self['logging']['file'] - except KeyError: + except (KeyError, TypeError): return self._logging_file def logging_set_verbose(self): @@ -191,28 +243,39 @@ class AppConfigUnit(ConfigUnit): def logging_is_verbose(self) -> bool: try: return bool(self['logging']['verbose']) - except KeyError: + except (KeyError, TypeError): return self._logging_verbose -class TranslationsUnit(BaseConfigUnit): - _name: str +class TranslationUnit(BaseConfigUnit): + pass - def __init__(self, - lang: str, - name: str): - super().__init__() - self._lang = lang - self._name = name - for dirname in CONFIG_DIRECTORIES: - if isdir(dirname): - filename = join(dirname, f'i18n-{lang}', f'{name}.yaml') - if isfile(filename): - self.load_from(filename) - break +class Translation: + LANGUAGES = ('en', 'ru') + DEFAULT_LANGUAGE = 'ru' - raise IOError(f'i18n-{lang}/{name}.yaml not found') + _langs: dict[str, TranslationUnit] + + def __init__(self, name: str): + super().__init__() + self._langs = {} + for lang in self.LANGUAGES: + for dirname in CONFIG_DIRECTORIES: + if isdir(dirname): + filename = join(dirname, f'i18n-{lang}', f'{name}.yaml') + if lang in self._langs: + raise RuntimeError(f'{name}: translation unit for lang \'{lang}\' already loaded') + self._langs[lang] = TranslationUnit() + self._langs[lang].load_from(filename) + diff = set() + for data in self._langs.values(): + diff ^= data.get().keys() + if len(diff) > 0: + raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff)) + + def get(self, lang: str) -> TranslationUnit: + return self._langs[lang] class Config: @@ -224,14 +287,19 @@ class Config: self.app_config = AppConfigUnit() def load_app(self, - name: Optional[Union[str, ConfigUnit, bool]] = None, + name: Optional[Union[str, AppConfigUnit, bool]] = None, use_cli=True, parser: ArgumentParser = None, no_config=False): + global app_config - if isinstance(name, ConfigUnit): + if not no_config \ + and not isinstance(name, str) \ + and not isinstance(name, bool) \ + and issubclass(name, AppConfigUnit) or name == AppConfigUnit: self.app_name = name.NAME self.app_config = name() + app_config = self.app_config else: self.app_name = name if isinstance(name, str) else None @@ -264,10 +332,11 @@ class Config: if not isinstance(name, ConfigUnit): if not no_config and path is None: - path = ConfigUnit.get_config_path(name=name) + path = ConfigUnit.get_config_path(name=self.app_name) if not no_config: self.app_config.load_from(path) + self.app_config.validate() setup_logging(self.app_config.logging_is_verbose(), self.app_config.logging_get_file(), @@ -278,7 +347,6 @@ class Config: config = Config() -app_config = config.app_config def is_development_mode() -> bool: diff --git a/src/home/database/__init__.py b/include/py/homekit/database/__init__.py index b50cbce..b50cbce 100644 --- a/src/home/database/__init__.py +++ b/include/py/homekit/database/__init__.py diff --git a/src/home/database/__init__.pyi b/include/py/homekit/database/__init__.pyi index 31aae5d..31aae5d 100644 --- a/src/home/database/__init__.pyi +++ b/include/py/homekit/database/__init__.pyi diff --git a/include/py/homekit/database/_base.py b/include/py/homekit/database/_base.py new file mode 100644 index 0000000..dcec9da --- /dev/null +++ b/include/py/homekit/database/_base.py @@ -0,0 +1,9 @@ +import os + + +def get_data_root_directory() -> str: + return os.path.join( + os.environ['HOME'], + '.config', + 'homekit', + 'data')
\ No newline at end of file diff --git a/src/home/database/bots.py b/include/py/homekit/database/bots.py index cde48b9..fb5f326 100644 --- a/src/home/database/bots.py +++ b/include/py/homekit/database/bots.py @@ -2,7 +2,6 @@ import pytz from .mysql import mysql_now, MySQLDatabase, datetime_fmt from ..api.types import ( - BotType, SoundSensorLocation ) from typing import Optional, List, Tuple @@ -27,15 +26,6 @@ class OpenwrtLogRecord: class BotsDatabase(MySQLDatabase): - def add_request(self, - bot: BotType, - user_id: int, - message: str): - with self.cursor() as cursor: - cursor.execute("INSERT INTO requests_log (user_id, message, bot, time) VALUES (%s, %s, %s, %s)", - (user_id, message, bot.name.lower(), mysql_now())) - self.commit() - def add_openwrt_logs(self, lines: List[Tuple[datetime, str]], access_point: int): diff --git a/src/home/database/clickhouse.py b/include/py/homekit/database/clickhouse.py index d0ec283..d0ec283 100644 --- a/src/home/database/clickhouse.py +++ b/include/py/homekit/database/clickhouse.py diff --git a/src/home/database/inverter.py b/include/py/homekit/database/inverter.py index fc3f74f..fc3f74f 100644 --- a/src/home/database/inverter.py +++ b/include/py/homekit/database/inverter.py diff --git a/src/home/database/inverter_time_formats.py b/include/py/homekit/database/inverter_time_formats.py index 7c37d30..7c37d30 100644 --- a/src/home/database/inverter_time_formats.py +++ b/include/py/homekit/database/inverter_time_formats.py diff --git a/src/home/database/mysql.py b/include/py/homekit/database/mysql.py index fe97cd4..fe97cd4 100644 --- a/src/home/database/mysql.py +++ b/include/py/homekit/database/mysql.py diff --git a/src/home/database/sensors.py b/include/py/homekit/database/sensors.py index 8155108..8155108 100644 --- a/src/home/database/sensors.py +++ b/include/py/homekit/database/sensors.py diff --git a/src/home/database/simple_state.py b/include/py/homekit/database/simple_state.py index cada9c8..2b8ebe7 100644 --- a/src/home/database/simple_state.py +++ b/include/py/homekit/database/simple_state.py @@ -2,24 +2,26 @@ import os import json import atexit +from ._base import get_data_root_directory + class SimpleState: def __init__(self, - file: str, - default: dict = None, - **kwargs): + name: str, + default: dict = None): if default is None: default = {} elif type(default) is not dict: raise TypeError('default must be dictionary') - if not os.path.exists(file): + path = os.path.join(get_data_root_directory(), name) + if not os.path.exists(path): self._data = default else: - with open(file, 'r') as f: + with open(path, 'r') as f: self._data = json.loads(f.read()) - self._file = file + self._file = path atexit.register(self.__cleanup) def __cleanup(self): diff --git a/src/home/database/sqlite.py b/include/py/homekit/database/sqlite.py index bfba929..1651a93 100644 --- a/src/home/database/sqlite.py +++ b/include/py/homekit/database/sqlite.py @@ -2,27 +2,31 @@ import sqlite3 import os.path import logging +from ._base import get_data_root_directory from ..config import config, is_development_mode -def _get_database_path(name: str, dbname: str) -> str: - return os.path.join(os.environ['HOME'], '.config', name, f'{dbname}.db') +def _get_database_path(name: str) -> str: + return os.path.join( + get_data_root_directory(), + f'{name}.db') class SQLiteBase: SCHEMA = 1 - def __init__(self, name=None, dbname='bot', check_same_thread=False): - db_path = config.get('db_path', default=None) - if db_path is None: + def __init__(self, name=None, path=None, check_same_thread=False): + if not path: if not name: name = config.app_name - if not dbname: - dbname = name - db_path = _get_database_path(name, dbname) + database_path = _get_database_path(name) + else: + database_path = path + if not os.path.exists(os.path.dirname(database_path)): + os.makedirs(os.path.dirname(database_path)) self.logger = logging.getLogger(self.__class__.__name__) - self.sqlite = sqlite3.connect(db_path, check_same_thread=check_same_thread) + self.sqlite = sqlite3.connect(database_path, check_same_thread=check_same_thread) if is_development_mode(): self.sql_logger = logging.getLogger(self.__class__.__name__) diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py new file mode 100644 index 0000000..d019e4c --- /dev/null +++ b/include/py/homekit/http/__init__.py @@ -0,0 +1,2 @@ +from .http import serve, ok, routes, HTTPServer, HTTPMethod +from aiohttp.web import FileResponse, StreamResponse, Request, Response
\ No newline at end of file diff --git a/src/home/http/http.py b/include/py/homekit/http/http.py index 3e70751..82c5aae 100644 --- a/src/home/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,8 +1,9 @@ import logging import asyncio +from enum import Enum from aiohttp import web -from aiohttp.web import Response +from aiohttp.web import Response, HTTPFound from aiohttp.web_exceptions import HTTPNotFound from ..util import stringify, format_tb, Addr @@ -20,6 +21,9 @@ async def errors_handler_middleware(request, handler): except HTTPNotFound: return web.json_response({'error': 'not found'}, status=404) + except HTTPFound as exc: + raise exc + except Exception as exc: _logger.exception(exc) data = { @@ -104,3 +108,8 @@ class HTTPServer: def plain(self, text: str): return Response(text=text, content_type='text/plain') + + +class HTTPMethod(Enum): + GET = 'GET' + POST = 'POST' diff --git a/src/home/inverter/__init__.py b/include/py/homekit/inverter/__init__.py index 8831ef3..8831ef3 100644 --- a/src/home/inverter/__init__.py +++ b/include/py/homekit/inverter/__init__.py diff --git a/include/py/homekit/inverter/config.py b/include/py/homekit/inverter/config.py new file mode 100644 index 0000000..0383e96 --- /dev/null +++ b/include/py/homekit/inverter/config.py @@ -0,0 +1,13 @@ +from ..config import ConfigUnit +from typing import Optional + + +class InverterdConfig(ConfigUnit): + NAME = 'inverterd' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'remote_addr': cls._addr_schema(required=True), + 'local_addr': cls._addr_schema(required=True), + } diff --git a/src/home/inverter/emulator.py b/include/py/homekit/inverter/emulator.py index e86b8bb..e86b8bb 100644 --- a/src/home/inverter/emulator.py +++ b/include/py/homekit/inverter/emulator.py diff --git a/src/home/inverter/inverter_wrapper.py b/include/py/homekit/inverter/inverter_wrapper.py index df2c2fc..df2c2fc 100644 --- a/src/home/inverter/inverter_wrapper.py +++ b/include/py/homekit/inverter/inverter_wrapper.py diff --git a/src/home/inverter/monitor.py b/include/py/homekit/inverter/monitor.py index 86f75ac..5955d92 100644 --- a/src/home/inverter/monitor.py +++ b/include/py/homekit/inverter/monitor.py @@ -25,7 +25,7 @@ def _pd_from_string(pd: str) -> BatteryPowerDirection: class MonitorConfig: def __getattr__(self, item): - return config['monitor'][item] + return config.app_config['monitor'][item] cfg = MonitorConfig() diff --git a/src/home/inverter/types.py b/include/py/homekit/inverter/types.py index 57021f1..57021f1 100644 --- a/src/home/inverter/types.py +++ b/include/py/homekit/inverter/types.py diff --git a/src/home/inverter/util.py b/include/py/homekit/inverter/util.py index a577e6a..a577e6a 100644 --- a/src/home/inverter/util.py +++ b/include/py/homekit/inverter/util.py diff --git a/src/home/media/__init__.py b/include/py/homekit/media/__init__.py index 6923105..6923105 100644 --- a/src/home/media/__init__.py +++ b/include/py/homekit/media/__init__.py diff --git a/src/home/media/__init__.pyi b/include/py/homekit/media/__init__.pyi index 77c2176..77c2176 100644 --- a/src/home/media/__init__.pyi +++ b/include/py/homekit/media/__init__.pyi diff --git a/src/home/media/node_client.py b/include/py/homekit/media/node_client.py index eb39898..eb39898 100644 --- a/src/home/media/node_client.py +++ b/include/py/homekit/media/node_client.py diff --git a/src/home/media/node_server.py b/include/py/homekit/media/node_server.py index 5d0803c..5d0803c 100644 --- a/src/home/media/node_server.py +++ b/include/py/homekit/media/node_server.py diff --git a/src/home/media/record.py b/include/py/homekit/media/record.py index cd7447a..cd7447a 100644 --- a/src/home/media/record.py +++ b/include/py/homekit/media/record.py diff --git a/src/home/media/record_client.py b/include/py/homekit/media/record_client.py index 322495c..322495c 100644 --- a/src/home/media/record_client.py +++ b/include/py/homekit/media/record_client.py diff --git a/src/home/media/storage.py b/include/py/homekit/media/storage.py index dd74ff8..dd74ff8 100644 --- a/src/home/media/storage.py +++ b/include/py/homekit/media/storage.py diff --git a/src/home/media/types.py b/include/py/homekit/media/types.py index acbc291..acbc291 100644 --- a/src/home/media/types.py +++ b/include/py/homekit/media/types.py diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py new file mode 100644 index 0000000..ea0930e --- /dev/null +++ b/include/py/homekit/modem/__init__.py @@ -0,0 +1,2 @@ +from .config import ModemsConfig +from .e3372 import E3372, MacroNetWorkType diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py new file mode 100644 index 0000000..16d1ba0 --- /dev/null +++ b/include/py/homekit/modem/config.py @@ -0,0 +1,29 @@ +from ..config import ConfigUnit, Translation +from typing import Optional + + +class ModemsConfig(ConfigUnit): + NAME = 'modems' + + _strings: Translation + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._strings = Translation('modems') + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'type': 'dict', + 'schema': { + 'ip': cls._addr_schema(required=True, only_ip=True), + 'gateway_ip': cls._addr_schema(required=False, only_ip=True), + 'legacy_auth': {'type': 'boolean', 'required': True} + } + } + + def getshortname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['short'] + + def getfullname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['full']
\ No newline at end of file diff --git a/include/py/homekit/modem/e3372.py b/include/py/homekit/modem/e3372.py new file mode 100644 index 0000000..f68db5a --- /dev/null +++ b/include/py/homekit/modem/e3372.py @@ -0,0 +1,253 @@ +import requests +import xml.etree.ElementTree as ElementTree + +from ..util import Addr +from enum import Enum +from ..http import HTTPMethod +from typing import Union + + +class Error(Enum): + ERROR_SYSTEM_NO_SUPPORT = 100002 + ERROR_SYSTEM_NO_RIGHTS = 100003 + ERROR_SYSTEM_BUSY = 100004 + ERROR_LOGIN_USERNAME_WRONG = 108001 + ERROR_LOGIN_PASSWORD_WRONG = 108002 + ERROR_LOGIN_ALREADY_LOGIN = 108003 + ERROR_LOGIN_USERNAME_PWD_WRONG = 108006 + ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007 + ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009 + ERROR_VOICE_BUSY = 120001 + ERROR_WRONG_TOKEN = 125001 + ERROR_WRONG_SESSION = 125002 + ERROR_WRONG_SESSION_TOKEN = 125003 + + +class WifiStatus(Enum): + WIFI_CONNECTING = '900' + WIFI_CONNECTED = '901' + WIFI_DISCONNECTED = '902' + WIFI_DISCONNECTING = '903' + + +class Cradle(Enum): + CRADLE_CONNECTING = '900' + CRADLE_CONNECTED = '901' + CRADLE_DISCONNECTED = '902' + CRADLE_DISCONNECTING = '903' + CRADLE_CONNECTFAILED = '904' + CRADLE_CONNECTSTATUSNULL = '905' + CRANDLE_CONNECTSTATUSERRO = '906' + + +class MacroEVDOLevel(Enum): + MACRO_EVDO_LEVEL_ZERO = '0' + MACRO_EVDO_LEVEL_ONE = '1' + MACRO_EVDO_LEVEL_TWO = '2' + MACRO_EVDO_LEVEL_THREE = '3' + MACRO_EVDO_LEVEL_FOUR = '4' + MACRO_EVDO_LEVEL_FIVE = '5' + + +class MacroNetWorkType(Enum): + MACRO_NET_WORK_TYPE_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_GSM = 1 + MACRO_NET_WORK_TYPE_GPRS = 2 + MACRO_NET_WORK_TYPE_EDGE = 3 + MACRO_NET_WORK_TYPE_WCDMA = 4 + MACRO_NET_WORK_TYPE_HSDPA = 5 + MACRO_NET_WORK_TYPE_HSUPA = 6 + MACRO_NET_WORK_TYPE_HSPA = 7 + MACRO_NET_WORK_TYPE_TDSCDMA = 8 + MACRO_NET_WORK_TYPE_HSPA_PLUS = 9 + MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10 + MACRO_NET_WORK_TYPE_EVDO_REV_A = 11 + MACRO_NET_WORK_TYPE_EVDO_REV_B = 12 + MACRO_NET_WORK_TYPE_1xRTT = 13 + MACRO_NET_WORK_TYPE_UMB = 14 + MACRO_NET_WORK_TYPE_1xEVDV = 15 + MACRO_NET_WORK_TYPE_3xRTT = 16 + MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17 + MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18 + MACRO_NET_WORK_TYPE_LTE = 19 + MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_EX_GSM = 1 + MACRO_NET_WORK_TYPE_EX_GPRS = 2 + MACRO_NET_WORK_TYPE_EX_EDGE = 3 + MACRO_NET_WORK_TYPE_EX_IS95A = 21 + MACRO_NET_WORK_TYPE_EX_IS95B = 22 + MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26 + MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36 + MACRO_NET_WORK_TYPE_EX_WCDMA = 41 + MACRO_NET_WORK_TYPE_EX_HSDPA = 42 + MACRO_NET_WORK_TYPE_EX_HSUPA = 43 + MACRO_NET_WORK_TYPE_EX_HSPA = 44 + MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45 + MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46 + MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61 + MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62 + MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63 + MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64 + MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65 + MACRO_NET_WORK_TYPE_EX_802_16E = 81 + MACRO_NET_WORK_TYPE_EX_LTE = 101 + + +def post_data_to_xml(data: dict, depth: int = 1) -> str: + if depth == 1: + return '<?xml version: "1.0" encoding="UTF-8"?>'+post_data_to_xml({'request': data}, depth+1) + + items = [] + for k, v in data.items(): + if isinstance(v, dict): + v = post_data_to_xml(v, depth+1) + elif isinstance(v, list): + raise TypeError('list type is unsupported here') + items.append(f'<{k}>{v}</{k}>') + + return ''.join(items) + + +class E3372: + _addr: Addr + _need_auth: bool + _legacy_token_auth: bool + _get_raw_data: bool + _headers: dict[str, str] + _authorized: bool + + def __init__(self, + addr: Addr, + need_auth: bool = True, + legacy_token_auth: bool = False, + get_raw_data: bool = False): + self._addr = addr + self._need_auth = need_auth + self._legacy_token_auth = legacy_token_auth + self._get_raw_data = get_raw_data + self._authorized = False + self._headers = {} + + @property + def device_information(self): + self.auth() + return self.request('device/information') + + @property + def device_signal(self): + self.auth() + return self.request('device/signal') + + @property + def monitoring_status(self): + self.auth() + return self.request('monitoring/status') + + @property + def notifications(self): + self.auth() + return self.request('monitoring/check-notifications') + + @property + def dialup_connection(self): + self.auth() + return self.request('dialup/connection') + + @property + def traffic_stats(self): + self.auth() + return self.request('monitoring/traffic-statistics') + + @property + def sms_count(self): + self.auth() + return self.request('sms/sms-count') + + def sms_send(self, phone: str, text: str): + self.auth() + return self.request('sms/send-sms', HTTPMethod.POST, { + 'Index': -1, + 'Phones': { + 'Phone': phone + }, + 'Sca': '', + 'Content': text, + 'Length': -1, + 'Reserved': 1, + 'Date': -1 + }) + + def sms_list(self, page: int = 1, count: int = 20, outbox: bool = False): + self.auth() + xml = self.request('sms/sms-list', HTTPMethod.POST, { + 'PageIndex': page, + 'ReadCount': count, + 'BoxType': 1 if not outbox else 2, + 'SortType': 0, + 'Ascending': 0, + 'UnreadPreferred': 1 if not outbox else 0 + }, return_body=True) + + root = ElementTree.fromstring(xml) + messages = [] + for message_elem in root.find('Messages').findall('Message'): + message_dict = {child.tag: child.text for child in message_elem} + messages.append(message_dict) + return messages + + def auth(self): + if self._authorized: + return + + if not self._legacy_token_auth: + data = self.request('webserver/SesTokInfo') + self._headers = { + 'Cookie': data['SesInfo'], + '__RequestVerificationToken': data['TokInfo'], + 'Content-Type': 'text/xml' + } + else: + data = self.request('webserver/token') + self._headers = { + '__RequestVerificationToken': data['token'], + 'Content-Type': 'text/xml' + } + + self._authorized = True + + def request(self, + method: str, + http_method: HTTPMethod = HTTPMethod.GET, + data: dict = {}, + return_body: bool = False) -> Union[str, dict]: + url = f'http://{self._addr}/api/{method}' + if http_method == HTTPMethod.POST: + data = post_data_to_xml(data) + f = requests.post + else: + data = None + f = requests.get + r = f(url, data=data, headers=self._headers) + r.raise_for_status() + r.encoding = 'utf-8' + + if return_body: + return r.text + + root = ElementTree.fromstring(r.text) + data_dict = {} + for elem in root: + data_dict[elem.tag] = elem.text + return data_dict diff --git a/src/home/mqtt/__init__.py b/include/py/homekit/mqtt/__init__.py index 707d59c..707d59c 100644 --- a/src/home/mqtt/__init__.py +++ b/include/py/homekit/mqtt/__init__.py diff --git a/src/home/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index 3f9dd09..8aa3bfe 100644 --- a/src/home/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -9,8 +9,8 @@ MqttCreds = namedtuple('MqttCreds', 'username, password') class MqttConfig(ConfigUnit): NAME = 'mqtt' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: addr_schema = { 'type': 'dict', 'required': True, @@ -64,8 +64,8 @@ class MqttConfig(ConfigUnit): class MqttNodesConfig(ConfigUnit): NAME = 'mqtt_nodes' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'common': { 'type': 'dict', @@ -92,6 +92,7 @@ class MqttNodesConfig(ConfigUnit): 'type': 'dict', 'schema': { 'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']}, + 'legacy_payload': {'type': 'boolean', 'required': False, 'default': False}, 'interval': {'type': 'integer'}, 'i2c_bus': {'type': 'integer'}, 'tcpserver': { @@ -104,9 +105,17 @@ class MqttNodesConfig(ConfigUnit): }, 'relay': { 'type': 'dict', - 'schema': {} + 'schema': { + 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid', 'cooler'], 'required': True}, + 'legacy_topics': {'type': 'boolean'} + } }, - 'password': {'type': 'string'} + 'password': {'type': 'string'}, + 'defines': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': {'type': ['string', 'integer']} + } } } } @@ -160,3 +169,15 @@ class MqttNodesConfig(ConfigUnit): else: resdict[name] = node return reslist if only_names else resdict + + def node_uses_legacy_temphum_data_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['temphum']['legacy_payload'] + except KeyError: + return False + + def node_uses_legacy_relay_power_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['relay']['legacy_topics'] + except KeyError: + return False diff --git a/src/home/mqtt/_module.py b/include/py/homekit/mqtt/_module.py index 80f27bb..80f27bb 100644 --- a/src/home/mqtt/_module.py +++ b/include/py/homekit/mqtt/_module.py diff --git a/src/home/mqtt/_mqtt.py b/include/py/homekit/mqtt/_mqtt.py index 3c893c1..47ee9ae 100644 --- a/src/home/mqtt/_mqtt.py +++ b/include/py/homekit/mqtt/_mqtt.py @@ -39,13 +39,14 @@ class Mqtt: self._client.username_pw_set(creds.username, creds.password) - def configure_tls(self): + def _configure_tls(self): ca_certs = os.path.realpath(os.path.join( os.path.dirname(os.path.realpath(__file__)), '..', '..', '..', - 'assets', + '..', + 'misc', 'mqtt_ca.crt' )) self._client.tls_set(ca_certs=ca_certs, @@ -53,6 +54,7 @@ class Mqtt: tls_version=ssl.PROTOCOL_TLSv1_2) def connect_and_loop(self, loop_forever=True): + self._configure_tls() addr = self._mqtt_config.local_addr() if self._is_server else self._mqtt_config.remote_addr() self._client.connect(addr.host, addr.port, 60) if loop_forever: diff --git a/src/home/mqtt/_node.py b/include/py/homekit/mqtt/_node.py index 4e259a4..4e259a4 100644 --- a/src/home/mqtt/_node.py +++ b/include/py/homekit/mqtt/_node.py diff --git a/src/home/mqtt/_payload.py b/include/py/homekit/mqtt/_payload.py index 58eeae3..58eeae3 100644 --- a/src/home/mqtt/_payload.py +++ b/include/py/homekit/mqtt/_payload.py diff --git a/src/home/mqtt/_util.py b/include/py/homekit/mqtt/_util.py index 390d463..390d463 100644 --- a/src/home/mqtt/_util.py +++ b/include/py/homekit/mqtt/_util.py diff --git a/src/home/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py index f858f88..5fc33fe 100644 --- a/src/home/mqtt/_wrapper.py +++ b/include/py/homekit/mqtt/_wrapper.py @@ -2,12 +2,13 @@ import paho.mqtt.client as mqtt from ._mqtt import Mqtt from ._node import MqttNode -from ..config import config from ..util import strgen class MqttWrapper(Mqtt): _nodes: list[MqttNode] + _connect_callbacks: list[callable] + _disconnect_callbacks: list[callable] def __init__(self, client_id: str, @@ -19,26 +20,47 @@ class MqttWrapper(Mqtt): super().__init__(clean_session=clean_session, client_id=client_id) self._nodes = [] + self._connect_callbacks = [] + self._disconnect_callbacks = [] self._topic_prefix = topic_prefix def on_connect(self, client: mqtt.Client, userdata, flags, rc): super().on_connect(client, userdata, flags, rc) for node in self._nodes: node.on_connect(self) + for f in self._connect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) def on_disconnect(self, client: mqtt.Client, userdata, rc): super().on_disconnect(client, userdata, rc) for node in self._nodes: node.on_disconnect() + for f in self._disconnect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) + def on_message(self, client: mqtt.Client, userdata, msg): try: topic = msg.topic + topic_node = topic[len(self._topic_prefix)+1:topic.find('/', len(self._topic_prefix)+1)] for node in self._nodes: - node.on_message(topic[len(f'{self._topic_prefix}/{node.id}/'):], msg.payload) + if node.id in ('+', topic_node): + node.on_message(topic[len(f'{self._topic_prefix}/{node.id}/'):], msg.payload) except Exception as e: self._logger.exception(str(e)) + def add_connect_callback(self, f: callable): + self._connect_callbacks.append(f) + + def add_disconnect_callback(self, f: callable): + self._disconnect_callbacks.append(f) + def add_node(self, node: MqttNode): self._nodes.append(node) if self._connected: diff --git a/src/home/mqtt/module/diagnostics.py b/include/py/homekit/mqtt/module/diagnostics.py index 5db5e99..5db5e99 100644 --- a/src/home/mqtt/module/diagnostics.py +++ b/include/py/homekit/mqtt/module/diagnostics.py diff --git a/src/home/mqtt/module/inverter.py b/include/py/homekit/mqtt/module/inverter.py index d927a06..29bde0a 100644 --- a/src/home/mqtt/module/inverter.py +++ b/include/py/homekit/mqtt/module/inverter.py @@ -11,7 +11,7 @@ from .._module import MqttModule from .._node import MqttNode from .._payload import MqttPayload, bit_field try: - from home.database import InverterDatabase + from homekit.database import InverterDatabase except: pass diff --git a/src/home/mqtt/module/ota.py b/include/py/homekit/mqtt/module/ota.py index cd34332..2f9b216 100644 --- a/src/home/mqtt/module/ota.py +++ b/include/py/homekit/mqtt/module/ota.py @@ -74,4 +74,4 @@ class MqttOtaModule(MqttModule): if not self._initialized: self._ota_request = (filename, qos) else: - self.do_push_ota(filename, qos) + self.do_push_ota(self._mqtt_node_ref.secret, filename, qos) diff --git a/src/home/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py index 5383fb6..5cbe09b 100644 --- a/src/home/mqtt/module/relay.py +++ b/include/py/homekit/mqtt/module/relay.py @@ -58,21 +58,27 @@ class MqttRelayState: class MqttRelayModule(MqttModule): + _legacy_topics: bool + + def __init__(self, legacy_topics=False, *args, **kwargs): + super().__init__(*args, **kwargs) + self._legacy_topics = legacy_topics + def on_connect(self, mqtt: MqttNode): super().on_connect(mqtt) - mqtt.subscribe_module('relay/switch', self) + mqtt.subscribe_module(self._get_switch_topic(), self) mqtt.subscribe_module('relay/status', self) - def switchpower(self, - enable: bool): + def switchpower(self, enable: bool): payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret, state=enable) - self._mqtt_node_ref.publish('relay/switch', payload=payload.pack()) + self._mqtt_node_ref.publish(self._get_switch_topic(), + payload=payload.pack()) def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: message = None - if topic == 'relay/switch': + if topic == self._get_switch_topic(): message = MqttPowerSwitchPayload.unpack(payload) elif topic == 'relay/status': message = MqttPowerStatusPayload.unpack(payload) @@ -80,3 +86,6 @@ class MqttRelayModule(MqttModule): if message is not None: self._logger.debug(message) return message + + def _get_switch_topic(self) -> str: + return 'relay/power' if self._legacy_topics else 'relay/switch' diff --git a/src/home/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py index fd02cca..6deccfe 100644 --- a/src/home/mqtt/module/temphum.py +++ b/include/py/homekit/mqtt/module/temphum.py @@ -10,8 +10,8 @@ MODULE_NAME = 'MqttTempHumModule' DATA_TOPIC = 'temphum/data' -class MqttTemphumDataPayload(MqttPayload): - FORMAT = '=ddb' +class MqttTemphumLegacyDataPayload(MqttPayload): + FORMAT = '=dd' UNPACKER = { 'temp': two_digits_precision, 'rh': two_digits_precision @@ -19,39 +19,26 @@ class MqttTemphumDataPayload(MqttPayload): temp: float rh: float - error: int -# class MqttTempHumNodes(HashableEnum): -# KBN_SH_HALL = auto() -# KBN_SH_BATHROOM = auto() -# KBN_SH_LIVINGROOM = auto() -# KBN_SH_BEDROOM = auto() -# -# KBN_BH_2FL = auto() -# KBN_BH_2FL_STREET = auto() -# KBN_BH_1FL_LIVINGROOM = auto() -# KBN_BH_1FL_BEDROOM = auto() -# KBN_BH_1FL_BATHROOM = auto() -# -# KBN_NH_1FL_INV = auto() -# KBN_NH_1FL_CENTER = auto() -# KBN_NH_1LF_KT = auto() -# KBN_NH_1FL_DS = auto() -# KBN_NH_1FS_EZ = auto() -# -# SPB_FLAT120_CABINET = auto() +class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload): + FORMAT = '=ddb' + error: int class MqttTempHumModule(MqttModule): + _legacy_payload: bool + def __init__(self, sensor: Optional[BaseSensor] = None, + legacy_payload=False, write_to_database=False, *args, **kwargs): if sensor is not None: kwargs['tick_interval'] = 10 super().__init__(*args, **kwargs) self._sensor = sensor + self._legacy_payload = legacy_payload def on_connect(self, mqtt: MqttNode): super().on_connect(mqtt) @@ -69,7 +56,7 @@ class MqttTempHumModule(MqttModule): rh = self._sensor.humidity() except: error = 1 - pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error) + pld = self._get_data_payload_cls()(temp=temp, rh=rh, error=error) self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack()) def handle_payload(self, @@ -77,6 +64,10 @@ class MqttTempHumModule(MqttModule): topic: str, payload: bytes) -> Optional[MqttPayload]: if topic == DATA_TOPIC: - message = MqttTemphumDataPayload.unpack(payload) + message = self._get_data_payload_cls().unpack(payload) self._logger.debug(message) return message + + def _get_data_payload_cls(self): + return MqttTemphumLegacyDataPayload if self._legacy_payload else MqttTemphumDataPayload + diff --git a/src/home/pio/__init__.py b/include/py/homekit/pio/__init__.py index 7216bc4..7216bc4 100644 --- a/src/home/pio/__init__.py +++ b/include/py/homekit/pio/__init__.py diff --git a/src/home/pio/exceptions.py b/include/py/homekit/pio/exceptions.py index a6afd20..a6afd20 100644 --- a/src/home/pio/exceptions.py +++ b/include/py/homekit/pio/exceptions.py diff --git a/src/home/pio/products.py b/include/py/homekit/pio/products.py index 388da03..3d5034f 100644 --- a/src/home/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -3,13 +3,14 @@ import logging from io import StringIO from collections import OrderedDict +from ..mqtt import MqttNodesConfig _logger = logging.getLogger(__name__) _products_dir = os.path.join( os.path.dirname(__file__), - '..', '..', '..', - 'platformio' + '..', '..', '..', '..', + 'pio' ) @@ -37,6 +38,13 @@ def platformio_ini(product_config: dict, debug=False, debug_network=False) -> str: node_id = build_specific_defines['CONFIG_NODE_ID'] + if node_id not in MqttNodesConfig().get_nodes().keys(): + raise ValueError(f'node id "{node_id}" is not specified in the config!') + + try: + node_defines = MqttNodesConfig().get_node(node_id)['defines'] + except KeyError: + node_defines = None # defines defines = { @@ -63,6 +71,8 @@ def platformio_ini(product_config: dict, if build_specific_defines: for k, v in build_specific_defines.items(): defines[k] = v + if node_defines: + defines = {**defines, **node_defines} defines = OrderedDict(sorted(defines.items(), key=lambda t: t[0])) # libs @@ -89,8 +99,10 @@ def platformio_ini(product_config: dict, buf.write(f'upload_port = {upload_port}\n') buf.write(f'monitor_speed = {monitor_speed}\n') if libs: - buf.write(f'lib_deps =') + buf.write(f'lib_deps =\n') for lib in libs: + if lib.startswith('homekit_'): + lib = 'file://../../include/pio/libs/'+lib[8:] buf.write(f' {lib}\n') buf.write(f'build_flags =\n') if defines: @@ -107,7 +119,7 @@ def platformio_ini(product_config: dict, if type(value) is str and not is_enum: buf.write('"\\"') buf.write('\n') - buf.write(f' -I../common/include') + buf.write(f' -I../../include/pio/include') buf.write(f'\nbuild_type = {build_type}') return buf.getvalue() diff --git a/src/home/relay/__init__.py b/include/py/homekit/relay/__init__.py index 406403d..406403d 100644 --- a/src/home/relay/__init__.py +++ b/include/py/homekit/relay/__init__.py diff --git a/src/home/relay/__init__.pyi b/include/py/homekit/relay/__init__.pyi index 7a4a2f4..7a4a2f4 100644 --- a/src/home/relay/__init__.pyi +++ b/include/py/homekit/relay/__init__.pyi diff --git a/src/home/relay/sunxi_h3_client.py b/include/py/homekit/relay/sunxi_h3_client.py index 8c8d6c4..8c8d6c4 100644 --- a/src/home/relay/sunxi_h3_client.py +++ b/include/py/homekit/relay/sunxi_h3_client.py diff --git a/src/home/relay/sunxi_h3_server.py b/include/py/homekit/relay/sunxi_h3_server.py index 1f33969..1f33969 100644 --- a/src/home/relay/sunxi_h3_server.py +++ b/include/py/homekit/relay/sunxi_h3_server.py diff --git a/src/home/soundsensor/__init__.py b/include/py/homekit/soundsensor/__init__.py index 30052f8..30052f8 100644 --- a/src/home/soundsensor/__init__.py +++ b/include/py/homekit/soundsensor/__init__.py diff --git a/src/home/soundsensor/__init__.pyi b/include/py/homekit/soundsensor/__init__.pyi index cb34972..cb34972 100644 --- a/src/home/soundsensor/__init__.pyi +++ b/include/py/homekit/soundsensor/__init__.pyi diff --git a/src/home/soundsensor/node.py b/include/py/homekit/soundsensor/node.py index 292452f..292452f 100644 --- a/src/home/soundsensor/node.py +++ b/include/py/homekit/soundsensor/node.py diff --git a/src/home/soundsensor/server.py b/include/py/homekit/soundsensor/server.py index a627390..a627390 100644 --- a/src/home/soundsensor/server.py +++ b/include/py/homekit/soundsensor/server.py diff --git a/src/home/soundsensor/server_client.py b/include/py/homekit/soundsensor/server_client.py index 7eef996..7eef996 100644 --- a/src/home/soundsensor/server_client.py +++ b/include/py/homekit/soundsensor/server_client.py diff --git a/src/home/telegram/__init__.py b/include/py/homekit/telegram/__init__.py index a68dae1..a68dae1 100644 --- a/src/home/telegram/__init__.py +++ b/include/py/homekit/telegram/__init__.py diff --git a/src/home/telegram/_botcontext.py b/include/py/homekit/telegram/_botcontext.py index f343eeb..a143bfe 100644 --- a/src/home/telegram/_botcontext.py +++ b/include/py/homekit/telegram/_botcontext.py @@ -1,6 +1,7 @@ from typing import Optional, List -from telegram import Update, ParseMode, User, CallbackQuery +from telegram import Update, User, CallbackQuery +from telegram.constants import ParseMode from telegram.ext import CallbackContext from ._botdb import BotDatabase @@ -26,25 +27,25 @@ class Context: self._store = store self._user_lang = None - def reply(self, text, markup=None): + async def reply(self, text, markup=None): if markup is None: markup = self._markup_getter(self) kwargs = dict(parse_mode=ParseMode.HTML) if not isinstance(markup, IgnoreMarkup): kwargs['reply_markup'] = markup - return self._update.message.reply_text(text, **kwargs) + return await self._update.message.reply_text(text, **kwargs) - def reply_exc(self, e: Exception) -> None: - self.reply(exc2text(e), markup=IgnoreMarkup()) + async def reply_exc(self, e: Exception) -> None: + await self.reply(exc2text(e), markup=IgnoreMarkup()) - def answer(self, text: str = None): - self.callback_query.answer(text) + async def answer(self, text: str = None): + await self.callback_query.answer(text) - def edit(self, text, markup=None): + async def edit(self, text, markup=None): kwargs = dict(parse_mode=ParseMode.HTML) if not isinstance(markup, IgnoreMarkup): kwargs['reply_markup'] = markup - self.callback_query.edit_message_text(text, **kwargs) + await self.callback_query.edit_message_text(text, **kwargs) @property def text(self) -> str: diff --git a/src/home/telegram/_botdb.py b/include/py/homekit/telegram/_botdb.py index 9e9cf94..4e1aec0 100644 --- a/src/home/telegram/_botdb.py +++ b/include/py/homekit/telegram/_botdb.py @@ -1,4 +1,4 @@ -from home.database.sqlite import SQLiteBase +from homekit.database.sqlite import SQLiteBase class BotDatabase(SQLiteBase): diff --git a/src/home/telegram/_botlang.py b/include/py/homekit/telegram/_botlang.py index f5f85bb..f5f85bb 100644 --- a/src/home/telegram/_botlang.py +++ b/include/py/homekit/telegram/_botlang.py diff --git a/src/home/telegram/_botutil.py b/include/py/homekit/telegram/_botutil.py index 6d1ee8f..4fbbf28 100644 --- a/src/home/telegram/_botutil.py +++ b/include/py/homekit/telegram/_botutil.py @@ -3,9 +3,6 @@ import traceback from html import escape from telegram import User -from home.api import WebAPIClient as APIClient -from home.api.types import BotType -from home.api.errors import ApiResponseError _logger = logging.getLogger(__name__) @@ -24,20 +21,6 @@ def user_any_name(user: User) -> str: return name -class ReportingHelper: - def __init__(self, client: APIClient, bot_type: BotType): - self.client = client - self.bot_type = bot_type - - def report(self, message, text: str = None) -> None: - if text is None: - text = message.text - try: - self.client.log_bot_request(self.bot_type, message.chat_id, text) - except ApiResponseError as error: - _logger.exception(error) - - def exc2text(e: Exception) -> str: tb = ''.join(traceback.format_tb(e.__traceback__)) return f'{e.__class__.__name__}: ' + escape(str(e)) + "\n\n" + escape(tb) diff --git a/src/home/telegram/aio.py b/include/py/homekit/telegram/aio.py index fc87c1c..fc87c1c 100644 --- a/src/home/telegram/aio.py +++ b/include/py/homekit/telegram/aio.py diff --git a/src/home/telegram/bot.py b/include/py/homekit/telegram/bot.py index 10bfe06..f5f620a 100644 --- a/src/home/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -5,54 +5,52 @@ import itertools from enum import Enum, auto from functools import wraps -from typing import Optional, Union, Tuple +from typing import Optional, Union, Tuple, Coroutine from telegram import Update, ReplyKeyboardMarkup from telegram.ext import ( - Updater, - Filters, - BaseFilter, + Application, + filters, CommandHandler, MessageHandler, CallbackQueryHandler, CallbackContext, ConversationHandler ) +from telegram.ext.filters import BaseFilter from telegram.error import TimedOut -from home.config import config -from home.api import WebAPIClient -from home.api.types import BotType +from homekit.config import config from ._botlang import lang, languages from ._botdb import BotDatabase -from ._botutil import ReportingHelper, exc2text, IgnoreMarkup, user_any_name +from ._botutil import exc2text, IgnoreMarkup from ._botcontext import Context +from .config import TelegramUserListType db: Optional[BotDatabase] = None _user_filter: Optional[BaseFilter] = None -_cancel_filter = Filters.text(lang.all('cancel')) -_back_filter = Filters.text(lang.all('back')) -_cancel_and_back_filter = Filters.text(lang.all('back') + lang.all('cancel')) +_cancel_filter = filters.Text(lang.all('cancel')) +_back_filter = filters.Text(lang.all('back')) +_cancel_and_back_filter = filters.Text(lang.all('back') + lang.all('cancel')) _logger = logging.getLogger(__name__) -_updater: Optional[Updater] = None -_reporting: Optional[ReportingHelper] = None -_exception_handler: Optional[callable] = None +_application: Optional[Application] = None +_exception_handler: Optional[Coroutine] = None _dispatcher = None _markup_getter: Optional[callable] = None -_start_handler_ref: Optional[callable] = None +_start_handler_ref: Optional[Coroutine] = None def text_filter(*args): if not _user_filter: raise RuntimeError('user_filter is not initialized') - return Filters.text(args[0] if isinstance(args[0], list) else [*args]) & _user_filter + return filters.Text(args[0] if isinstance(args[0], list) else [*args]) & _user_filter -def _handler_of_handler(*args, **kwargs): +async def _handler_of_handler(*args, **kwargs): self = None context = None update = None @@ -99,7 +97,7 @@ def _handler_of_handler(*args, **kwargs): if self: _args.insert(0, self) - result = f(*_args, **kwargs) + result = await f(*_args, **kwargs) return result if not return_with_context else (result, ctx) except Exception as e: @@ -107,7 +105,7 @@ def _handler_of_handler(*args, **kwargs): if not _exception_handler(e, ctx) and not isinstance(e, TimedOut): _logger.exception(e) if not ctx.is_callback_context(): - ctx.reply_exc(e) + await ctx.reply_exc(e) else: notify_user(ctx.user_id, exc2text(e)) else: @@ -117,10 +115,10 @@ def _handler_of_handler(*args, **kwargs): def handler(**kwargs): def inner(f): @wraps(f) - def _handler(*args, **inner_kwargs): + async def _handler(*args, **inner_kwargs): if 'argument' in kwargs and kwargs['argument'] == 'message_key': inner_kwargs['argument'] = 'message_key' - return _handler_of_handler(f=f, *args, **inner_kwargs) + return await _handler_of_handler(f=f, *args, **inner_kwargs) messages = [] texts = [] @@ -139,43 +137,43 @@ def handler(**kwargs): new_messages = list(itertools.chain.from_iterable([lang.all(m) for m in messages])) texts += new_messages texts = list(set(texts)) - _updater.dispatcher.add_handler( + _application.add_handler( MessageHandler(text_filter(*texts), _handler), group=0 ) if 'command' in kwargs: - _updater.dispatcher.add_handler(CommandHandler(kwargs['command'], _handler), group=0) + _application.add_handler(CommandHandler(kwargs['command'], _handler), group=0) if 'callback' in kwargs: - _updater.dispatcher.add_handler(CallbackQueryHandler(_handler, pattern=kwargs['callback']), group=0) + _application.add_handler(CallbackQueryHandler(_handler, pattern=kwargs['callback']), group=0) return _handler return inner -def simplehandler(f: callable): +def simplehandler(f: Coroutine): @wraps(f) - def _handler(*args, **kwargs): - return _handler_of_handler(f=f, *args, **kwargs) + async def _handler(*args, **kwargs): + return await _handler_of_handler(f=f, *args, **kwargs) return _handler def callbackhandler(*args, **kwargs): def inner(f): @wraps(f) - def _handler(*args, **kwargs): - return _handler_of_handler(f=f, *args, **kwargs) + async def _handler(*args, **kwargs): + return await _handler_of_handler(f=f, *args, **kwargs) pattern_kwargs = {} if kwargs['callback'] != '*': pattern_kwargs['pattern'] = kwargs['callback'] - _updater.dispatcher.add_handler(CallbackQueryHandler(_handler, **pattern_kwargs), group=0) + _application.add_handler(CallbackQueryHandler(_handler, **pattern_kwargs), group=0) return _handler return inner -def exceptionhandler(f: callable): +async def exceptionhandler(f: callable): global _exception_handler if _exception_handler: _logger.warning('exception handler already set, we will overwrite it') @@ -198,10 +196,10 @@ def convinput(state, is_enter=False, **kwargs): ) @wraps(f) - def _impl(*args, **kwargs): - result, ctx = _handler_of_handler(f=f, *args, **kwargs, return_with_context=True) + async def _impl(*args, **kwargs): + result, ctx = await _handler_of_handler(f=f, *args, **kwargs, return_with_context=True) if result == conversation.END: - start(ctx) + await start(ctx) return result return _impl @@ -252,7 +250,7 @@ class conversation: handlers.append(MessageHandler(text_filter(lang.all(message) if 'messages_lang_completed' not in kwargs else message), self.make_invoker(target_state))) if 'regex' in kwargs: - handlers.append(MessageHandler(Filters.regex(kwargs['regex']) & _user_filter, f)) + handlers.append(MessageHandler(filters.Regex(kwargs['regex']) & _user_filter, f)) if 'command' in kwargs: handlers.append(CommandHandler(kwargs['command'], f, _user_filter)) @@ -268,7 +266,7 @@ class conversation: return self.invoke(state, ctx) return _invoke - def invoke(self, state, ctx: Context): + async def invoke(self, state, ctx: Context): self._logger.debug(f'invoke, state={state}') for item in dir(self): f = getattr(self, item) @@ -276,7 +274,7 @@ class conversation: continue cd = f.__dict__['_conv_data'] if cd['enter'] and cd['state'] == state: - return cd['orig_f'](self, ctx) + return await cd['orig_f'](self, ctx) raise RuntimeError(f'invoke: failed to find method for state {state}') @@ -327,21 +325,21 @@ class conversation: @staticmethod @simplehandler - def invalid(ctx: Context): - ctx.reply(ctx.lang('invalid_input'), markup=IgnoreMarkup()) + async def invalid(ctx: Context): + await ctx.reply(ctx.lang('invalid_input'), markup=IgnoreMarkup()) # return 0 # FIXME is this needed @simplehandler - def cancel(self, ctx: Context): - start(ctx) + async def cancel(self, ctx: Context): + await start(ctx) self.set_user_state(ctx.user_id, None) return conversation.END @simplehandler - def back(self, ctx: Context): + async def back(self, ctx: Context): cur_state = self.get_user_state(ctx.user_id) if cur_state is None: - start(ctx) + await start(ctx) self.set_user_state(ctx.user_id, None) return conversation.END @@ -364,14 +362,14 @@ class conversation: # buttons.insert(0, [ctx.lang('back')]) buttons.append([ctx.lang('back')]) - def reply(self, - ctx: Context, - state: Union[int, Enum], - text: str, - buttons: Optional[list], - with_cancel=False, - with_back=False, - buttons_lang_completed=False): + async def reply(self, + ctx: Context, + state: Union[int, Enum], + text: str, + buttons: Optional[list], + with_cancel=False, + with_back=False, + buttons_lang_completed=False): if buttons: new_buttons = [] @@ -402,7 +400,7 @@ class conversation: self.add_back_button(ctx, new_buttons) markup = ReplyKeyboardMarkup(new_buttons, one_time_keyboard=True) if new_buttons else IgnoreMarkup() - ctx.reply(text, markup=markup) + await ctx.reply(text, markup=markup) self.set_user_state(ctx.user_id, state) return state @@ -411,7 +409,7 @@ class LangConversation(conversation): START, = range(1) @conventer(START, command='lang') - def entry(self, ctx: Context): + async def entry(self, ctx: Context): self._logger.debug(f'current language: {ctx.user_lang}') buttons = [] @@ -419,11 +417,11 @@ class LangConversation(conversation): buttons.append(name) markup = ReplyKeyboardMarkup([buttons, [ctx.lang('cancel')]], one_time_keyboard=False) - ctx.reply(ctx.lang('select_language'), markup=markup) + await ctx.reply(ctx.lang('select_language'), markup=markup) return self.START @convinput(START, messages=lang.languages) - def input(self, ctx: Context): + async def input(self, ctx: Context): selected_lang = None for key, value in languages.items(): if value == ctx.text: @@ -434,30 +432,34 @@ class LangConversation(conversation): raise ValueError('could not find the language') db.set_user_lang(ctx.user_id, selected_lang) - ctx.reply(ctx.lang('saved'), markup=IgnoreMarkup()) + await ctx.reply(ctx.lang('saved'), markup=IgnoreMarkup()) return self.END def initialize(): global _user_filter - global _updater + global _application + # global _updater global _dispatcher # init user_filter - if 'users' in config['bot']: - _logger.info('allowed users: ' + str(config['bot']['users'])) - _user_filter = Filters.user(config['bot']['users']) + _user_ids = config.app_config.get_user_ids() + if len(_user_ids) > 0: + _logger.info('allowed users: ' + str(_user_ids)) + _user_filter = filters.User(_user_ids) else: - _user_filter = Filters.all # not sure if this is correct + _user_filter = filters.ALL # not sure if this is correct - # init updater - _updater = Updater(config['bot']['token'], - request_kwargs={'read_timeout': 6, 'connect_timeout': 7}) + _application = Application.builder()\ + .token(config.app_config.get('bot.token'))\ + .connect_timeout(7)\ + .read_timeout(6)\ + .build() # transparently log all messages - _updater.dispatcher.add_handler(MessageHandler(Filters.all & _user_filter, _logging_message_handler), group=10) - _updater.dispatcher.add_handler(CallbackQueryHandler(_logging_callback_handler), group=10) + # _application.dispatcher.add_handler(MessageHandler(filters.ALL & _user_filter, _logging_message_handler), group=10) + # _application.dispatcher.add_handler(CallbackQueryHandler(_logging_callback_handler), group=10) def run(start_handler=None, any_handler=None): @@ -473,103 +475,97 @@ def run(start_handler=None, any_handler=None): _start_handler_ref = start_handler - _updater.dispatcher.add_handler(LangConversation().get_handler(), group=0) - _updater.dispatcher.add_handler(CommandHandler('start', simplehandler(start_handler), _user_filter)) - _updater.dispatcher.add_handler(MessageHandler(Filters.all & _user_filter, any_handler)) + _application.add_handler(LangConversation().get_handler(), group=0) + _application.add_handler(CommandHandler('start', + callback=simplehandler(start_handler), + filters=_user_filter)) + _application.add_handler(MessageHandler(filters.ALL & _user_filter, any_handler)) - _updater.start_polling() - _updater.idle() + _application.run_polling() def add_conversation(conv: conversation) -> None: - _updater.dispatcher.add_handler(conv.get_handler(), group=0) + _application.add_handler(conv.get_handler(), group=0) def add_handler(h): - _updater.dispatcher.add_handler(h, group=0) + _application.add_handler(h, group=0) -def start(ctx: Context): - return _start_handler_ref(ctx) +async def start(ctx: Context): + return await _start_handler_ref(ctx) -def _default_start_handler(ctx: Context): +async def _default_start_handler(ctx: Context): if 'start_message' not in lang: - return ctx.reply('Please define start_message or override start()') - ctx.reply(ctx.lang('start_message')) + return await ctx.reply('Please define start_message or override start()') + await ctx.reply(ctx.lang('start_message')) @simplehandler -def _default_any_handler(ctx: Context): +async def _default_any_handler(ctx: Context): if 'invalid_command' not in lang: - return ctx.reply('Please define invalid_command or override any()') - ctx.reply(ctx.lang('invalid_command')) + return await ctx.reply('Please define invalid_command or override any()') + await ctx.reply(ctx.lang('invalid_command')) -def _logging_message_handler(update: Update, context: CallbackContext): - if _reporting: - _reporting.report(update.message) +# def _logging_message_handler(update: Update, context: CallbackContext): +# if _reporting: +# _reporting.report(update.message) +# +# +# def _logging_callback_handler(update: Update, context: CallbackContext): +# if _reporting: +# _reporting.report(update.callback_query.message, text=update.callback_query.data) -def _logging_callback_handler(update: Update, context: CallbackContext): - if _reporting: - _reporting.report(update.callback_query.message, text=update.callback_query.data) - - -def enable_logging(bot_type: BotType): - api = WebAPIClient(timeout=3) - api.enable_async() - - global _reporting - _reporting = ReportingHelper(api, bot_type) - - -def notify_all(text_getter: callable, - exclude: Tuple[int] = ()) -> None: - if 'notify_users' not in config['bot']: - _logger.error('notify_all() called but no notify_users directive found in the config') +async def notify_all(text_getter: callable, + exclude: Tuple[int] = ()) -> None: + notify_user_ids = config.app_config.get_user_ids(TelegramUserListType.NOTIFY) + if not notify_user_ids: + _logger.error('notify_all() called but no notify_users defined in the config') return - for user_id in config['bot']['notify_users']: + for user_id in notify_user_ids: if user_id in exclude: continue text = text_getter(db.get_user_lang(user_id)) - _updater.bot.send_message(chat_id=user_id, - text=text, - parse_mode='HTML') + await _application.bot.send_message(chat_id=user_id, + text=text, + parse_mode='HTML') -def notify_user(user_id: int, text: Union[str, Exception], **kwargs) -> None: +async def notify_user(user_id: int, text: Union[str, Exception], **kwargs) -> None: if isinstance(text, Exception): text = exc2text(text) - _updater.bot.send_message(chat_id=user_id, - text=text, - parse_mode='HTML', - **kwargs) + await _application.bot.send_message(chat_id=user_id, + text=text, + parse_mode='HTML', + **kwargs) -def send_photo(user_id, **kwargs): - _updater.bot.send_photo(chat_id=user_id, **kwargs) +async def send_photo(user_id, **kwargs): + await _application.bot.send_photo(chat_id=user_id, **kwargs) -def send_audio(user_id, **kwargs): - _updater.bot.send_audio(chat_id=user_id, **kwargs) +async def send_audio(user_id, **kwargs): + await _application.bot.send_audio(chat_id=user_id, **kwargs) -def send_file(user_id, **kwargs): - _updater.bot.send_document(chat_id=user_id, **kwargs) +async def send_file(user_id, **kwargs): + await _application.bot.send_document(chat_id=user_id, **kwargs) -def edit_message_text(user_id, message_id, *args, **kwargs): - _updater.bot.edit_message_text(chat_id=user_id, - message_id=message_id, - parse_mode='HTML', - *args, **kwargs) +async def edit_message_text(user_id, message_id, *args, **kwargs): + await _application.bot.edit_message_text(chat_id=user_id, + message_id=message_id, + parse_mode='HTML', + *args, **kwargs) -def delete_message(user_id, message_id): - _updater.bot.delete_message(chat_id=user_id, message_id=message_id) +async def delete_message(user_id, message_id): + await _application.bot.delete_message(chat_id=user_id, message_id=message_id) def set_database(_db: BotDatabase): diff --git a/src/home/telegram/config.py b/include/py/homekit/telegram/config.py index 8ca4c09..4d54854 100644 --- a/src/home/telegram/config.py +++ b/include/py/homekit/telegram/config.py @@ -12,11 +12,11 @@ class TelegramUserListType(Enum): class TelegramUserIdsConfig(ConfigUnit): NAME = 'telegram_user_ids' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { - 'type': 'dict', - 'schema': {'type': 'int'} + 'roottype': 'dict', + 'type': 'integer' } @@ -32,8 +32,8 @@ def _user_id_mapper(user: Union[str, int]) -> int: class TelegramChatsConfig(ConfigUnit): NAME = 'telegram_chats' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'dict', 'schema': { @@ -44,22 +44,22 @@ class TelegramChatsConfig(ConfigUnit): class TelegramBotConfig(ConfigUnit, ABC): - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'bot': { 'type': 'dict', 'schema': { 'token': {'type': 'string', 'required': True}, - TelegramUserListType.USERS: {**TelegramBotConfig._userlist_schema(), 'required': True}, - TelegramUserListType.NOTIFY: TelegramBotConfig._userlist_schema(), + TelegramUserListType.USERS.value: {**TelegramBotConfig._userlist_schema(), 'required': True}, + TelegramUserListType.NOTIFY.value: TelegramBotConfig._userlist_schema(), } } } @staticmethod def _userlist_schema() -> dict: - return {'type': 'list', 'schema': {'type': ['string', 'int']}} + return {'type': 'list', 'schema': {'type': ['string', 'integer']}} @staticmethod def custom_validator(data): @@ -72,4 +72,7 @@ class TelegramBotConfig(ConfigUnit, ABC): def get_user_ids(self, ult: TelegramUserListType = TelegramUserListType.USERS) -> list[int]: - return list(map(_user_id_mapper, self['bot'][ult.value]))
\ No newline at end of file + try: + return list(map(_user_id_mapper, self['bot'][ult.value])) + except KeyError: + return [] diff --git a/src/home/telegram/telegram.py b/include/py/homekit/telegram/telegram.py index 2f94f93..f42363e 100644 --- a/src/home/telegram/telegram.py +++ b/include/py/homekit/telegram/telegram.py @@ -2,25 +2,27 @@ import requests import logging from typing import Tuple -from ..config import config - +from .config import TelegramChatsConfig +_chats = TelegramChatsConfig() _logger = logging.getLogger(__name__) def send_message(text: str, - parse_mode: str = None, - disable_web_page_preview: bool = False): - data, token = _send_telegram_data(text, parse_mode, disable_web_page_preview) + chat: str, + parse_mode: str = 'HTML', + disable_web_page_preview: bool = False,): + data, token = _send_telegram_data(text, chat, parse_mode, disable_web_page_preview) req = requests.post('https://api.telegram.org/bot%s/sendMessage' % token, data=data) return req.json() -def send_photo(filename: str): +def send_photo(filename: str, chat: str): + chat_data = _chats[chat] data = { - 'chat_id': config['telegram']['chat_id'], + 'chat_id': chat_data['id'], } - token = config['telegram']['token'] + token = chat_data['token'] url = f'https://api.telegram.org/bot{token}/sendPhoto' with open(filename, "rb") as fd: @@ -29,19 +31,19 @@ def send_photo(filename: str): def _send_telegram_data(text: str, + chat: str, parse_mode: str = None, disable_web_page_preview: bool = False) -> Tuple[dict, str]: + chat_data = _chats[chat] data = { - 'chat_id': config['telegram']['chat_id'], + 'chat_id': chat_data['id'], 'text': text } if parse_mode is not None: data['parse_mode'] = parse_mode - elif 'parse_mode' in config['telegram']: - data['parse_mode'] = config['telegram']['parse_mode'] - if disable_web_page_preview or 'disable_web_page_preview' in config['telegram']: + if disable_web_page_preview: data['disable_web_page_preview'] = 1 - return data, config['telegram']['token'] + return data, chat_data['token'] diff --git a/src/home/temphum/__init__.py b/include/py/homekit/temphum/__init__.py index 46d14e6..46d14e6 100644 --- a/src/home/temphum/__init__.py +++ b/include/py/homekit/temphum/__init__.py diff --git a/src/home/temphum/base.py b/include/py/homekit/temphum/base.py index 602cab7..602cab7 100644 --- a/src/home/temphum/base.py +++ b/include/py/homekit/temphum/base.py diff --git a/src/home/temphum/i2c.py b/include/py/homekit/temphum/i2c.py index 7d8e2e3..7d8e2e3 100644 --- a/src/home/temphum/i2c.py +++ b/include/py/homekit/temphum/i2c.py diff --git a/src/home/util.py b/include/py/homekit/util.py index 155b4ef..f718291 100644 --- a/src/home/util.py +++ b/include/py/homekit/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import socket import time @@ -6,18 +8,104 @@ import traceback import logging import string import random +import re +import os +import ipaddress from collections import namedtuple from enum import Enum from datetime import datetime -from typing import Tuple, Optional, List +from typing import Optional, List from zlib import adler32 -Addr = namedtuple('Addr', 'host, port') - logger = logging.getLogger(__name__) +def validate_ipv4_or_hostname(address: str, raise_exception: bool = False) -> bool: + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', address): + parts = address.split('.') + if all(0 <= int(part) < 256 for part in parts): + return True + else: + if raise_exception: + raise ValueError(f"invalid IPv4 address: {address}") + return False + + if re.match(r'^[a-zA-Z0-9.-]+$', address): + return True + else: + if raise_exception: + raise ValueError(f"invalid hostname: {address}") + return False + + +def validate_ipv4(address: str) -> bool: + try: + ipaddress.IPv6Address(address) + return True + except ipaddress.AddressValueError: + return False + + +def validate_mac_address(mac_address: str) -> bool: + mac_pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' + if re.match(mac_pattern, mac_address): + return True + else: + return False + + +class Addr: + host: str + port: Optional[int] + + def __init__(self, host: str, port: Optional[int] = None): + self.host = host + self.port = port + + @classmethod + def fromstring(cls, addr: str, port_required=True) -> Addr: + if port_required: + colons = addr.count(':') + if colons != 1: + raise ValueError('invalid host:port format') + + if not colons: + host = addr + port = None + else: + host, port = addr.split(':') + else: + port = None + host = addr + + validate_ipv4_or_hostname(host, raise_exception=True) + + if port is not None: + port = int(port) + if not 0 <= port <= 65535: + raise ValueError(f'invalid port {port}') + + return Addr(host, port) + + @classmethod + def fromipstring(cls, addr: str) -> Addr: + return cls.fromstring(addr, port_required=False) + + def __str__(self): + buf = self.host + if self.port is not None: + buf += ':'+str(self.port) + return buf + + def __repr__(self): + return self.__str__() + + def __iter__(self): + yield self.host + yield self.port + + # https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks def chunks(lst, n): """Yield successive n-sized chunks from lst.""" @@ -46,21 +134,6 @@ def ipv4_valid(ip: str) -> bool: return False -def parse_addr(addr: str) -> Addr: - if addr.count(':') != 1: - raise ValueError('invalid host:port format') - - host, port = addr.split(':') - if not ipv4_valid(host): - raise ValueError('invalid ipv4 address') - - port = int(port) - if not 0 <= port <= 65535: - raise ValueError('invalid port') - - return Addr(host, port) - - def strgen(n: int): return ''.join(random.choices(string.ascii_letters + string.digits, k=n)) @@ -192,6 +265,24 @@ def filesize_fmt(num, suffix="B") -> str: return f"{num:.1f} Yi{suffix}" +def seconds_to_human_readable_string(seconds: int) -> str: + days, remainder = divmod(seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)} day{'s' if days > 1 else ''}") + if hours > 0: + parts.append(f"{int(hours)} hour{'s' if hours > 1 else ''}") + if minutes > 0: + parts.append(f"{int(minutes)} minute{'s' if minutes > 1 else ''}") + if seconds > 0: + parts.append(f"{int(seconds)} second{'s' if seconds > 1 else ''}") + + return ' '.join(parts) + + class HashableEnum(Enum): def hash(self) -> int: return adler32(self.name.encode()) @@ -201,4 +292,10 @@ def next_tick_gen(freq): t = time.time() while True: t += freq - yield max(t - time.time(), 0)
\ No newline at end of file + yield max(t - time.time(), 0) + + +def homekit_path(*args) -> str: + return os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', *args) + ) diff --git a/pyA20/__init__.pyi b/include/py/pyA20/__init__.pyi index e69de29..e69de29 100644 --- a/pyA20/__init__.pyi +++ b/include/py/pyA20/__init__.pyi diff --git a/pyA20/gpio/connector.pyi b/include/py/pyA20/gpio/connector.pyi index 12b2b6e..12b2b6e 100644 --- a/pyA20/gpio/connector.pyi +++ b/include/py/pyA20/gpio/connector.pyi diff --git a/pyA20/gpio/gpio.pyi b/include/py/pyA20/gpio/gpio.pyi index 225fcbe..225fcbe 100644 --- a/pyA20/gpio/gpio.pyi +++ b/include/py/pyA20/gpio/gpio.pyi diff --git a/pyA20/gpio/port.pyi b/include/py/pyA20/gpio/port.pyi index 17f69fe..17f69fe 100644 --- a/pyA20/gpio/port.pyi +++ b/include/py/pyA20/gpio/port.pyi diff --git a/pyA20/port.pyi b/include/py/pyA20/port.pyi index e69de29..e69de29 100644 --- a/pyA20/port.pyi +++ b/include/py/pyA20/port.pyi diff --git a/src/syncleo/__init__.py b/include/py/syncleo/__init__.py index 32563a5..32563a5 100644 --- a/src/syncleo/__init__.py +++ b/include/py/syncleo/__init__.py diff --git a/src/syncleo/kettle.py b/include/py/syncleo/kettle.py index d6e0dd6..d6e0dd6 100644 --- a/src/syncleo/kettle.py +++ b/include/py/syncleo/kettle.py diff --git a/src/syncleo/protocol.py b/include/py/syncleo/protocol.py index 36a1a8f..36a1a8f 100644 --- a/src/syncleo/protocol.py +++ b/include/py/syncleo/protocol.py diff --git a/localwebsite/classes/E3372.php b/localwebsite/classes/E3372.php deleted file mode 100644 index a3ce80c..0000000 --- a/localwebsite/classes/E3372.php +++ /dev/null @@ -1,310 +0,0 @@ -<?php - -class E3372 -{ - - const WIFI_CONNECTING = '900'; - const WIFI_CONNECTED = '901'; - const WIFI_DISCONNECTED = '902'; - const WIFI_DISCONNECTING = '903'; - - const CRADLE_CONNECTING = '900'; - const CRADLE_CONNECTED = '901'; - const CRADLE_DISCONNECTED = '902'; - const CRADLE_DISCONNECTING = '903'; - const CRADLE_CONNECTFAILED = '904'; - const CRADLE_CONNECTSTATUSNULL = '905'; - const CRANDLE_CONNECTSTATUSERRO = '906'; - - const MACRO_EVDO_LEVEL_ZERO = '0'; - const MACRO_EVDO_LEVEL_ONE = '1'; - const MACRO_EVDO_LEVEL_TWO = '2'; - const MACRO_EVDO_LEVEL_THREE = '3'; - const MACRO_EVDO_LEVEL_FOUR = '4'; - const MACRO_EVDO_LEVEL_FIVE = '5'; - - // CurrentNetworkType - const MACRO_NET_WORK_TYPE_NOSERVICE = 0; - const MACRO_NET_WORK_TYPE_GSM = 1; - const MACRO_NET_WORK_TYPE_GPRS = 2; - const MACRO_NET_WORK_TYPE_EDGE = 3; - const MACRO_NET_WORK_TYPE_WCDMA = 4; - const MACRO_NET_WORK_TYPE_HSDPA = 5; - const MACRO_NET_WORK_TYPE_HSUPA = 6; - const MACRO_NET_WORK_TYPE_HSPA = 7; - const MACRO_NET_WORK_TYPE_TDSCDMA = 8; - const MACRO_NET_WORK_TYPE_HSPA_PLUS = 9; - const MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10; - const MACRO_NET_WORK_TYPE_EVDO_REV_A = 11; - const MACRO_NET_WORK_TYPE_EVDO_REV_B = 12; - const MACRO_NET_WORK_TYPE_1xRTT = 13; - const MACRO_NET_WORK_TYPE_UMB = 14; - const MACRO_NET_WORK_TYPE_1xEVDV = 15; - const MACRO_NET_WORK_TYPE_3xRTT = 16; - const MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17; - const MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18; - const MACRO_NET_WORK_TYPE_LTE = 19; - const MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0; - const MACRO_NET_WORK_TYPE_EX_GSM = 1; - const MACRO_NET_WORK_TYPE_EX_GPRS = 2; - const MACRO_NET_WORK_TYPE_EX_EDGE = 3; - const MACRO_NET_WORK_TYPE_EX_IS95A = 21; - const MACRO_NET_WORK_TYPE_EX_IS95B = 22; - const MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25; - const MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26; - const MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32; - const MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35; - const MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36; - const MACRO_NET_WORK_TYPE_EX_WCDMA = 41; - const MACRO_NET_WORK_TYPE_EX_HSDPA = 42; - const MACRO_NET_WORK_TYPE_EX_HSUPA = 43; - const MACRO_NET_WORK_TYPE_EX_HSPA = 44; - const MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45; - const MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46; - const MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61; - const MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62; - const MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63; - const MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64; - const MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65; - const MACRO_NET_WORK_TYPE_EX_802_16E = 81; - const MACRO_NET_WORK_TYPE_EX_LTE = 101; - - - const ERROR_SYSTEM_NO_SUPPORT = 100002; - const ERROR_SYSTEM_NO_RIGHTS = 100003; - const ERROR_SYSTEM_BUSY = 100004; - const ERROR_LOGIN_USERNAME_WRONG = 108001; - const ERROR_LOGIN_PASSWORD_WRONG = 108002; - const ERROR_LOGIN_ALREADY_LOGIN = 108003; - const ERROR_LOGIN_USERNAME_PWD_WRONG = 108006; - const ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007; - const ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009; - const ERROR_VOICE_BUSY = 120001; - const ERROR_WRONG_TOKEN = 125001; - const ERROR_WRONG_SESSION = 125002; - const ERROR_WRONG_SESSION_TOKEN = 125003; - - private string $host; - private array $headers = []; - private bool $authorized = false; - private bool $useLegacyTokenAuth = false; - - public function __construct(string $host, bool $legacy_token_auth = false) { - $this->host = $host; - $this->useLegacyTokenAuth = $legacy_token_auth; - } - - public function auth() { - if ($this->authorized) - return; - - if (!$this->useLegacyTokenAuth) { - $data = $this->request('webserver/SesTokInfo'); - $this->headers = [ - 'Cookie: '.$data['SesInfo'], - '__RequestVerificationToken: '.$data['TokInfo'], - 'Content-Type: text/xml' - ]; - } else { - $data = $this->request('webserver/token'); - $this->headers = [ - '__RequestVerificationToken: '.$data['token'], - 'Content-Type: text/xml' - ]; - } - $this->authorized = true; - } - - public function getDeviceInformation() { - $this->auth(); - return $this->request('device/information'); - } - - public function getDeviceSignal() { - $this->auth(); - return $this->request('device/signal'); - } - - public function getMonitoringStatus() { - $this->auth(); - return $this->request('monitoring/status'); - } - - public function getNotifications() { - $this->auth(); - return $this->request('monitoring/check-notifications'); - } - - public function getDialupConnection() { - $this->auth(); - return $this->request('dialup/connection'); - } - - public function getTrafficStats() { - $this->auth(); - return $this->request('monitoring/traffic-statistics'); - } - - public function getSMSCount() { - $this->auth(); - return $this->request('sms/sms-count'); - } - - public function sendSMS(string $phone, string $text) { - $this->auth(); - return $this->request('sms/send-sms', 'POST', [ - 'Index' => -1, - 'Phones' => [ - 'Phone' => $phone - ], - 'Sca' => '', - 'Content' => $text, - 'Length' => -1, - 'Reserved' => 1, - 'Date' => -1 - ]); - } - - public function getSMSList(int $page = 1, int $count = 20, bool $outbox = false) { - $this->auth(); - $xml = $this->request('sms/sms-list', 'POST', [ - 'PageIndex' => $page, - 'ReadCount' => $count, - 'BoxType' => !$outbox ? 1 : 2, - 'SortType' => 0, - 'Ascending' => 0, - 'UnreadPreferred' => !$outbox ? 1 : 0 - ], true); - $xml = simplexml_load_string($xml); - - $messages = []; - foreach ($xml->Messages->Message as $message) { - $dt = DateTime::createFromFormat("Y-m-d H:i:s", (string)$message->Date); - $messages[] = [ - 'date' => (string)$message->Date, - 'timestamp' => $dt->getTimestamp(), - 'phone' => (string)$message->Phone, - 'content' => (string)$message->Content - ]; - } - return $messages; - } - - private function xmlToAssoc(string $xml): array { - $xml = new SimpleXMLElement($xml); - $data = []; - foreach ($xml as $name => $value) { - $data[$name] = (string)$value; - } - return $data; - } - - private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) { - $ch = curl_init(); - $url = 'http://'.$this->host.'/api/'.$method; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - if (!empty($this->headers)) - curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); - if ($http_method == 'POST') { - curl_setopt($ch, CURLOPT_POST, true); - - $post_data = $this->postDataToXML($data); - // debugLog('post_data:', $post_data); - - if (!empty($data)) - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - } - $body = curl_exec($ch); - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($code != 200) - throw new Exception('e3372 host returned code '.$code); - - curl_close($ch); - return $return_body ? $body : $this->xmlToAssoc($body); - } - - private function postDataToXML(array $data, int $depth = 1): string { - if ($depth == 1) - return '<?xml version: "1.0" encoding="UTF-8"?>'.$this->postDataToXML(['request' => $data], $depth+1); - - $items = []; - foreach ($data as $key => $value) { - if (is_array($value)) - $value = $this->postDataToXML($value, $depth+1); - $items[] = "<{$key}>{$value}</{$key}>"; - } - - return implode('', $items); - } - - public static function getNetworkTypeLabel($type): string { - switch ((int)$type) { - case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT'; - case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB'; - case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV'; - case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO'; - case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE'; - case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A'; - case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B'; - case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E'; - case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE'; - default: return '?'; - } - } - -} diff --git a/localwebsite/classes/GPIORelaydClient.php b/localwebsite/classes/GPIORelaydClient.php deleted file mode 100644 index 89c8dc9..0000000 --- a/localwebsite/classes/GPIORelaydClient.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php - -class GPIORelaydClient extends MySimpleSocketClient { - - const STATUS_ON = 'on'; - const STATUS_OFF = 'off'; - - public function setStatus(string $status) { - $this->send($status); - return $this->recv(); - } - - public function getStatus() { - $this->send('get'); - return $this->recv(); - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/InverterdClient.php b/localwebsite/classes/InverterdClient.php deleted file mode 100644 index b68b784..0000000 --- a/localwebsite/classes/InverterdClient.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php - -class InverterdClient extends MySimpleSocketClient { - - /** - * @throws Exception - */ - public function setProtocol(int $v): string - { - $this->send("v $v"); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function setFormat(string $fmt): string - { - $this->send("format $fmt"); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function exec(string $command, array $arguments = []): string - { - $buf = "exec $command"; - if (!empty($arguments)) { - foreach ($arguments as $arg) - $buf .= " $arg"; - } - $this->send($buf); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function recv() - { - $recv_buf = ''; - $buf = ''; - - while (true) { - $result = socket_recv($this->sock, $recv_buf, 1024, 0); - if ($result === false) - throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError()); - - // peer disconnected - if ($result === 0) - break; - - $buf .= $recv_buf; - if (endsWith($buf, "\r\n\r\n")) - break; - } - - $response = explode("\r\n", $buf); - $status = array_shift($response); - if (!in_array($status, ['ok', 'err'])) - throw new Exception(__METHOD__.': unexpected status ('.$status.')'); - if ($status == 'err') - throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]); - - return trim(implode("\r\n", $response)); - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/MyOpenWrtUtils.php b/localwebsite/classes/MyOpenWrtUtils.php index 6bdfec2..c140fa1 100644 --- a/localwebsite/classes/MyOpenWrtUtils.php +++ b/localwebsite/classes/MyOpenWrtUtils.php @@ -61,6 +61,14 @@ class MyOpenWrtUtils { return $list; } + public static function setUpstream(string $ip) { + return self::run(['homekit-set-default-upstream', $ip]); + } + + public static function getDefaultRoute() { + return self::run(['get-default-route']); + } + // // http functions @@ -128,4 +136,4 @@ class MyOpenWrtUtils { ]; } -}
\ No newline at end of file +} diff --git a/localwebsite/handlers/InverterHandler.php b/localwebsite/handlers/InverterHandler.php deleted file mode 100644 index 7098e2c..0000000 --- a/localwebsite/handlers/InverterHandler.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -class InverterHandler extends RequestHandler -{ - - public function __construct() { - parent::__construct(); - $this->tpl->add_static('inverter.js'); - } - - public function GET_status_page() { - $inv = $this->getClient(); - - $status = jsonDecode($inv->exec('get-status'))['data']; - $rated = jsonDecode($inv->exec('get-rated'))['data']; - - $this->tpl->set([ - 'status' => $status, - 'rated' => $rated, - 'html' => $this->renderStatusHtml($status, $rated) - ]); - $this->tpl->set_title('Инвертор'); - $this->tpl->render_page('inverter_page.twig'); - } - - public function GET_set_osp() { - list($osp) = $this->input('e:value(=sub|sbu)'); - $inv = $this->getClient(); - try { - $inv->exec('set-output-source-priority', [strtoupper($osp)]); - } catch (Exception $e) { - die('Ошибка: '.jsonDecode($e->getMessage())['message']); - } - redirect('/inverter/'); - } - - public function GET_status_ajax() { - $inv = $this->getClient(); - $status = jsonDecode($inv->exec('get-status'))['data']; - $rated = jsonDecode($inv->exec('get-rated'))['data']; - ajax_ok(['html' => $this->renderStatusHtml($status, $rated)]); - } - - protected function renderStatusHtml(array $status, array $rated) { - $power_direction = strtolower($status['battery_power_direction']); - $power_direction = preg_replace('/ge$/', 'ging', $power_direction); - - $charging_rate = ''; - if ($power_direction == 'charging') - $charging_rate = sprintf(' @ %s %s', - $status['battery_charge_current']['value'], - $status['battery_charge_current']['unit']); - else if ($power_direction == 'discharging') - $charging_rate = sprintf(' @ %s %s', - $status['battery_discharge_current']['value'], - $status['battery_discharge_current']['unit']); - - $html = sprintf('<b>Battery:</b> %s %s', - $status['battery_voltage']['value'], - $status['battery_voltage']['unit']); - $html .= sprintf(' (%s%s, ', - $status['battery_capacity']['value'], - $status['battery_capacity']['unit']); - $html .= sprintf('%s%s)', - $power_direction, - $charging_rate); - - $html .= "\n".sprintf('<b>Load:</b> %s %s', - $status['ac_output_active_power']['value'], - $status['ac_output_active_power']['unit']); - $html .= sprintf(' (%s%%)', - $status['output_load_percent']['value']); - - if ($status['pv1_input_power']['value'] > 0) - $html .= "\n".sprintf('<b>Input power:</b> %s %s', - $status['pv1_input_power']['value'], - $status['pv1_input_power']['unit']); - - if ($status['grid_voltage']['value'] > 0 or $status['grid_freq']['value'] > 0) { - $html .= "\n".sprintf('<b>AC input:</b> %s %s', - $status['grid_voltage']['value'], - $status['grid_voltage']['unit']); - $html .= sprintf(', %s %s', - $status['grid_freq']['value'], - $status['grid_freq']['unit']); - } - - $html .= "\n".sprintf('<b>Priority:</b> %s', - $rated['output_source_priority']); - - return nl2br($html); - } - - protected function getClient(): InverterdClient { - global $config; - $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']); - $inv->setFormat('json'); - return $inv; - } - - -}
\ No newline at end of file diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php index 10b4426..efaca22 100644 --- a/localwebsite/handlers/MiscHandler.php +++ b/localwebsite/handlers/MiscHandler.php @@ -3,17 +3,6 @@ class MiscHandler extends RequestHandler { - public function GET_main() { - global $config; - $this->tpl->set_title('Главная'); - $this->tpl->set([ - 'grafana_sensors_url' => $config['grafana_sensors_url'], - 'grafana_inverter_url' => $config['grafana_inverter_url'], - 'cameras' => $config['cam_list']['labels'] - ]); - $this->tpl->render_page('index.twig'); - } - public function GET_sensors_page() { global $config; @@ -30,26 +19,6 @@ class MiscHandler extends RequestHandler $this->tpl->render_page('sensors.twig'); } - public function GET_pump_page() { - global $config; - - list($set) = $this->input('set'); - $client = new GPIORelaydClient($config['pump_host'], $config['pump_port']); - - if ($set == GPIORelaydClient::STATUS_ON || $set == GPIORelaydClient::STATUS_OFF) { - $client->setStatus($set); - redirect('/pump/'); - } - - $status = $client->getStatus(); - - $this->tpl->set([ - 'status' => $status - ]); - $this->tpl->set_title('Насос'); - $this->tpl->render_page('pump.twig'); - } - public function GET_cams() { global $config; @@ -157,12 +126,4 @@ class MiscHandler extends RequestHandler } } - public function GET_debug() { - print_r($_SERVER); - } - - public function GET_phpinfo() { - phpinfo(); - } - -}
\ No newline at end of file +} diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index b54b82c..94ad75b 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -7,76 +7,11 @@ use libphonenumber\PhoneNumberUtil; class ModemHandler extends RequestHandler { - public function __construct() - { - parent::__construct(); - $this->tpl->add_static('modem.js'); - } - - public function GET_status_page() { - global $config; - - $this->tpl->set([ - 'modems' => $config['modems'], - 'js_modems' => array_keys($config['modems']), - ]); - - $this->tpl->set_title('Состояние модемов'); - $this->tpl->render_page('modem_status_page.twig'); - } - - public function GET_status_get_ajax() { - global $config; - list($id) = $this->input('id'); - if (!isset($config['modems'][$id])) - ajax_error('invalid modem id: '.$id); - - $modem_data = self::getModemData( - $config['modems'][$id]['ip'], - $config['modems'][$id]['legacy_token_auth']); - - ajax_ok([ - 'html' => $this->tpl->render('modem_data.twig', [ - 'loading' => false, - 'modem' => $id, - 'modem_data' => $modem_data - ]) - ]); - } - - public function GET_verbose_page() { - global $config; - - list($modem) = $this->input('modem'); - if (!$modem) - $modem = array_key_first($config['modems']); - - list($signal, $status, $traffic, $device, $dialup_conn) = self::getModemData( - $config['modems'][$modem]['ip'], - $config['modems'][$modem]['legacy_token_auth'], - true); - - $data = [ - ['Signal', $signal], - ['Connection', $status], - ['Traffic', $traffic], - ['Device info', $device], - ['Dialup connection', $dialup_conn] - ]; - $this->tpl->set([ - 'data' => $data, - 'modem_name' => $config['modems'][$modem]['label'], - ]); - $this->tpl->set_title('Подробная информация о модеме '.$modem); - $this->tpl->render_page('modem_verbose_page.twig'); - } - - public function GET_routing_smallhome_page() { global $config; list($error) = $this->input('error'); - $upstream = self::getCurrentSmallHomeUpstream(); + $upstream = self::getCurrentUpstream(); $current_upstream = [ 'key' => $upstream, @@ -98,12 +33,13 @@ class ModemHandler extends RequestHandler if (!isset($config['modems'][$new_upstream])) redirect('/routing/?error='.urlencode('invalid upstream')); - $current_upstream = self::getCurrentSmallHomeUpstream(); + $current_upstream = self::getCurrentUpstream(); if ($current_upstream != $new_upstream) { - if ($current_upstream != $config['routing_default']) - MyOpenWrtUtils::ipsetDel($current_upstream, $config['routing_smallhome_ip']); - if ($new_upstream != $config['routing_default']) - MyOpenWrtUtils::ipsetAdd($new_upstream, $config['routing_smallhome_ip']); + if ($new_upstream == 'mts-il') + $new_upstream_ip = '192.168.88.1'; + else + $new_upstream_ip = $config['modems'][$new_upstream]['ip']; + MyOpenWrtUtils::setUpstream($new_upstream_ip); } redirect('/routing/'); @@ -159,119 +95,16 @@ class ModemHandler extends RequestHandler $this->tpl->render_page('routing_dhcp_page.twig'); } - public function GET_sms() { - global $config; - - list($selected, $is_outbox, $error, $sent) = $this->input('modem, b:outbox, error, b:sent'); - if (!$selected) - $selected = array_key_first($config['modems']); - - $cfg = $config['modems'][$selected]; - $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']); - $messages = $e3372->getSMSList(1, 20, $is_outbox); - - $this->tpl->set([ - 'modems_list' => array_keys($config['modems']), - 'modems' => $config['modems'], - 'selected_modem' => $selected, - 'messages' => $messages, - 'is_outbox' => $is_outbox, - 'error' => $error, - 'is_sent' => $sent - ]); - - $direction = $is_outbox ? 'исходящие' : 'входящие'; - $this->tpl->set_title('SMS-сообщения ('.$direction.', '.$selected.')'); - $this->tpl->render_page('sms_page.twig'); - } - - public function POST_sms() { - global $config; - - list($selected, $is_outbox, $phone, $text) = $this->input('modem, b:outbox, phone, text'); - if (!$selected) - $selected = array_key_first($config['modems']); - - $return_url = '/sms/?modem='.$selected; - if ($is_outbox) - $return_url .= '&outbox=1'; - - $go_back = function(?string $error = null) use ($return_url) { - if (!is_null($error)) - $return_url .= '&error='.urlencode($error); - else - $return_url .= '&sent=1'; - redirect($return_url); - }; - - $phone = preg_replace('/\s+/', '', $phone); - - // при отправке смс на короткие номера не надо использовать libphonenumber и вот это вот всё - if (strlen($phone) > 4) { - $country = null; - if (!startsWith($phone, '+')) - $country = 'RU'; - - $phoneUtil = PhoneNumberUtil::getInstance(); - try { - $number = $phoneUtil->parse($phone, $country); - } catch (NumberParseException $e) { - debugError(__METHOD__.': failed to parse number '.$phone.': '.$e->getMessage()); - $go_back('Неверный номер ('.$e->getMessage().')'); - return; - } - - if (!$phoneUtil->isValidNumber($number)) { - $go_back('Неверный номер'); - return; - } - - $phone = $phoneUtil->format($number, PhoneNumberFormat::E164); - } - - $cfg = $config['modems'][$selected]; - $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']); - - $result = $e3372->sendSMS($phone, $text); - debugLog($result); - - $go_back(); - } - - protected static function getModemData(string $ip, - bool $need_auth = true, - bool $get_raw_data = false): array { - $modem = new E3372($ip, $need_auth); - - $signal = $modem->getDeviceSignal(); - $status = $modem->getMonitoringStatus(); - $traffic = $modem->getTrafficStats(); - - if ($get_raw_data) { - $device_info = $modem->getDeviceInformation(); - $dialup_conn = $modem->getDialupConnection(); - return [$signal, $status, $traffic, $device_info, $dialup_conn]; - } else { - return [ - 'type' => e3372::getNetworkTypeLabel($status['CurrentNetworkType']), - 'level' => $status['SignalIcon'] ?? 0, - 'rssi' => $signal['rssi'], - 'sinr' => $signal['sinr'], - 'connected_time' => secondsToTime($traffic['CurrentConnectTime']), - 'downloaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentDownload'])), - 'uploaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentUpload'])), - ]; - } - } - - protected static function getCurrentSmallHomeUpstream() { + protected static function getCurrentUpstream() { global $config; + $default_route = MyOpenWrtUtils::getDefaultRoute(); + if ($default_route == '192.168.88.1') + $default_route = $config['modems']['mts-il']['ip']; $upstream = null; - $ip_sets = MyOpenWrtUtils::ipsetListAll(); - foreach ($ip_sets as $set => $ips) { - if (in_array($config['routing_smallhome_ip'], $ips)) { - $upstream = $set; + foreach ($config['modems'] as $modem_name => $modem_data) { + if ($default_route == $modem_data['ip']) { + $upstream = $modem_name; break; } } @@ -294,4 +127,4 @@ class ModemHandler extends RequestHandler redirect('/routing/ipsets/?error='.urlencode('invalid ip/network: '.$ip)); } -}
\ No newline at end of file +} diff --git a/localwebsite/htdocs/assets/inverter.js b/localwebsite/htdocs/assets/inverter.js deleted file mode 100644 index 72d985c..0000000 --- a/localwebsite/htdocs/assets/inverter.js +++ /dev/null @@ -1,15 +0,0 @@ -var Inverter = { - poll: function () { - setInterval(this._tick, 1000); - }, - - _tick: function() { - ajax.get('/inverter/status.ajax') - .then(({response}) => { - if (response) { - var el = document.getElementById('inverter_status'); - el.innerHTML = response.html; - } - }); - } -};
\ No newline at end of file diff --git a/localwebsite/htdocs/assets/modem.js b/localwebsite/htdocs/assets/modem.js deleted file mode 100644 index 9fdb91d..0000000 --- a/localwebsite/htdocs/assets/modem.js +++ /dev/null @@ -1,29 +0,0 @@ -var ModemStatus = { - _modems: [], - - init: function(modems) { - for (var i = 0; i < modems.length; i++) { - var modem = modems[i]; - this._modems.push(new ModemStatusUpdater(modem)); - } - } -}; - - -function ModemStatusUpdater(id) { - this.id = id; - this.elem = ge('modem_data_'+id); - this.fetch(); -} -extend(ModemStatusUpdater.prototype, { - fetch: function() { - ajax.get('/modem/get.ajax', { - id: this.id - }).then(({response}) => { - var {html} = response; - this.elem.innerHTML = html; - - // TODO enqueue rerender - }); - }, -});
\ No newline at end of file diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php index d6034e6..cd32132 100644 --- a/localwebsite/htdocs/index.php +++ b/localwebsite/htdocs/index.php @@ -4,11 +4,6 @@ require_once __DIR__.'/../init.php'; $router = new router; -// modem -$router->add('modem/', 'Modem status_page'); -$router->add('modem/verbose/', 'Modem verbose_page'); -$router->add('modem/get.ajax', 'Modem status_get_ajax'); - $router->add('routing/', 'Modem routing_smallhome_page'); $router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch'); $router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page'); @@ -18,15 +13,11 @@ $router->add('sms/', 'Modem sms'); // $router->add('modem/set.ajax', 'Modem ctl_set_ajax'); // inverter -$router->add('inverter/', 'Inverter status_page'); $router->add('inverter/set-osp/', 'Inverter set_osp'); -$router->add('inverter/status.ajax', 'Inverter status_ajax'); // misc $router->add('/', 'Misc main'); $router->add('sensors/', 'Misc sensors_page'); -$router->add('pump/', 'Misc pump_page'); -$router->add('phpinfo/', 'Misc phpinfo'); $router->add('cams/', 'Misc cams'); $router->add('cams/([\d,]+)/', 'Misc cams id=$(1)'); $router->add('cams/stat/', 'Misc cams_stat'); diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig index bbf6802..b28a078 100644 --- a/localwebsite/templates-web/index.twig +++ b/localwebsite/templates-web/index.twig @@ -20,8 +20,8 @@ <h6 class="mt-4">Другое</h6> <ul class="list-group list-group-flush"> - <li class="list-group-item"><a href="/inverter/">Инвертор</a> (<a href="{{ grafana_inverter_url }}">Grafana</a>)</li> - <li class="list-group-item"><a href="/pump/">Насос</a></li> + <li class="list-group-item"><a href="/inverter/">Инвертор</a> (<a href="/inverter/?alt=1">alt</a>, <a href="{{ grafana_inverter_url }}">Grafana</a>)</li> + <li class="list-group-item"><a href="/pump/">Насос</a> (<a href="/pump/?alt=1">alt</a>)</li> <li class="list-group-item"><a href="/sensors/">Датчики</a> (<a href="{{ grafana_sensors_url }}">Grafana</a>)</li> </ul> @@ -32,4 +32,4 @@ {% endfor %} <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li> </ul> -</div>
\ No newline at end of file +</div> diff --git a/localwebsite/templates-web/inverter_page.twig b/localwebsite/templates-web/inverter_page.twig deleted file mode 100644 index c51e1bf..0000000 --- a/localwebsite/templates-web/inverter_page.twig +++ /dev/null @@ -1,20 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {text: "Инвертор" } - ] -} %} - -<h6 class="text-primary">Статус</h6> -<div id="inverter_status"> - {{ html|raw }} -</div> - -<div class="pt-3"> - <a href="/inverter/set-osp/?value={{ rated.output_source_priority == 'Solar-Battery-Utility' ? 'sub' : 'sbu' }}"> - <button type="button" class="btn btn-primary">Переключить на <b>{{ rated.output_source_priority == 'Solar-Battery-Utility' ? 'Solar-Utility-Battery' : 'Solar-Battery-Utility' }}</b></button> - </a> -</div> - -{% js %} -Inverter.poll(); -{% endjs %}
\ No newline at end of file diff --git a/localwebsite/templates-web/modem_data.twig b/localwebsite/templates-web/modem_data.twig deleted file mode 100644 index a2c00e5..0000000 --- a/localwebsite/templates-web/modem_data.twig +++ /dev/null @@ -1,14 +0,0 @@ -{% if not loading %} - <span class="text-secondary">Сигнал:</span> {% include 'signal_level.twig' with {'level': modem_data.level} %}<br> - <span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br> - <span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/> - {% if modem_data.sinr %} - <span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/> - {% endif %} - <span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br> - <span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} - <br> - <a href="/modem/verbose/?modem={{ modem }}">Подробная информация</a> -{% else %} - {% include 'spinner.twig' %} -{% endif %}
\ No newline at end of file diff --git a/localwebsite/templates-web/modem_status_page.twig b/localwebsite/templates-web/modem_status_page.twig deleted file mode 100644 index 3f20b86..0000000 --- a/localwebsite/templates-web/modem_status_page.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {text: "Модемы" } - ] -} %} - -{% for modem_key, modem in modems %} - <h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modem.label }}</h6> - <div id="modem_data_{{ modem_key }}"> - {% include 'modem_data.twig' with { - loading: true, - modem: modem_key - } %} - </div> -{% endfor %} - -{% js %} -ModemStatus.init({{ js_modems|json_encode|raw }}); -{% endjs %} diff --git a/localwebsite/templates-web/modem_verbose_page.twig b/localwebsite/templates-web/modem_verbose_page.twig deleted file mode 100644 index 3b4c25e..0000000 --- a/localwebsite/templates-web/modem_verbose_page.twig +++ /dev/null @@ -1,15 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {link: '/modem/', text: "Модемы" }, - {text: modem_name} - ] -} %} - -{% for item in data %} - {% set item_name = item[0] %} - {% set item_data = item[1] %} - <h6 class="text-primary mt-4">{{ item_name }}</h6> - {% for k, v in item_data %} - {{ k }} = {{ v }}<br> - {% endfor %} -{% endfor %}
\ No newline at end of file diff --git a/localwebsite/templates-web/routing_header.twig b/localwebsite/templates-web/routing_header.twig index 8cb5f47..7d07d0a 100644 --- a/localwebsite/templates-web/routing_header.twig +++ b/localwebsite/templates-web/routing_header.twig @@ -5,7 +5,7 @@ } %} {% set routing_tabs = [ - {tab: 'smallhome', url: '/routing/', label: 'Маленький дом'}, + {tab: 'smallhome', url: '/routing/', label: 'Интернет'}, {tab: 'ipsets', url: '/routing/ipsets/', label: 'Правила'}, {tab: 'dhcp', url: '/routing/dhcp/', label: 'DHCP'} ] %} diff --git a/localwebsite/templates-web/spinner.twig b/localwebsite/templates-web/spinner.twig deleted file mode 100644 index 2d629ea..0000000 --- a/localwebsite/templates-web/spinner.twig +++ /dev/null @@ -1,14 +0,0 @@ -<div class="sk-fading-circle"> - <div class="sk-circle1 sk-circle"></div> - <div class="sk-circle2 sk-circle"></div> - <div class="sk-circle3 sk-circle"></div> - <div class="sk-circle4 sk-circle"></div> - <div class="sk-circle5 sk-circle"></div> - <div class="sk-circle6 sk-circle"></div> - <div class="sk-circle7 sk-circle"></div> - <div class="sk-circle8 sk-circle"></div> - <div class="sk-circle9 sk-circle"></div> - <div class="sk-circle10 sk-circle"></div> - <div class="sk-circle11 sk-circle"></div> - <div class="sk-circle12 sk-circle"></div> -</div>
\ No newline at end of file diff --git a/misc/home_linux_boards/etc/default/homekit_ipcam_server b/misc/home_linux_boards/etc/default/homekit_ipcam_server new file mode 100644 index 0000000..e5ee2a3 --- /dev/null +++ b/misc/home_linux_boards/etc/default/homekit_ipcam_server @@ -0,0 +1,2 @@ +LISTEN="0.0.0.0:8320" +DATABASE_PATH="/data1/ipcam_server.db"
\ No newline at end of file diff --git a/misc/scripts/ipcam_capture_restart.sh b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_capture_restart.sh index 85144da..85144da 100644..100755 --- a/misc/scripts/ipcam_capture_restart.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_capture_restart.sh diff --git a/misc/scripts/ipcam_rtsp2hls_restart.sh b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_rtsp2hls_restart.sh index 61ee623..61ee623 100644..100755 --- a/misc/scripts/ipcam_rtsp2hls_restart.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_rtsp2hls_restart.sh diff --git a/misc/scripts/make_netns_per_upstream.sh b/misc/home_linux_boards/usr/local/bin/homekit_make_netns_per_upstream.sh index fb152fa..fb152fa 100644..100755 --- a/misc/scripts/make_netns_per_upstream.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_make_netns_per_upstream.sh diff --git a/tools/sunxi-h3-i2c-reset.sh b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_h3_i2c_reset.sh index e654dfb..e654dfb 100644..100755 --- a/tools/sunxi-h3-i2c-reset.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_h3_i2c_reset.sh diff --git a/tools/sunxi-setup-amixer.sh b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh index 5746514..5746514 100755 --- a/tools/sunxi-setup-amixer.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh diff --git a/tools/sync-recordings-to-remote.sh b/misc/home_linux_boards/usr/local/bin/homekit_sync_recordings_to_remote.sh index cf979d1..cf979d1 100755 --- a/tools/sync-recordings-to-remote.sh +++ b/misc/home_linux_boards/usr/local/bin/homekit_sync_recordings_to_remote.sh diff --git a/assets/mqtt_ca.crt b/misc/mqtt_ca.crt index 045ae10..045ae10 100644 --- a/assets/mqtt_ca.crt +++ b/misc/mqtt_ca.crt diff --git a/tools/clickhouse-backup.sh b/misc/remote_server/usr/local/bin/clickhouse_backup.sh index 6e938e4..6e938e4 100644..100755 --- a/tools/clickhouse-backup.sh +++ b/misc/remote_server/usr/local/bin/clickhouse_backup.sh diff --git a/tools/remove-old-recordings.sh b/misc/remote_server/usr/local/bin/remove_old_recordings.sh index d376572..d376572 100644..100755 --- a/tools/remove-old-recordings.sh +++ b/misc/remote_server/usr/local/bin/remove_old_recordings.sh diff --git a/platformio/dumb_mqtt/src/main.cpp b/pio/dumb_mqtt/src/main.cpp index eefc165..eefc165 100644 --- a/platformio/dumb_mqtt/src/main.cpp +++ b/pio/dumb_mqtt/src/main.cpp diff --git a/platformio/relayctl/src/main.cpp b/pio/relayctl/src/main.cpp index c399641..c399641 100644 --- a/platformio/relayctl/src/main.cpp +++ b/pio/relayctl/src/main.cpp diff --git a/platformio/temphum/src/main.cpp b/pio/temphum/src/main.cpp index 2df8638..2df8638 100644 --- a/platformio/temphum/src/main.cpp +++ b/pio/temphum/src/main.cpp diff --git a/platformio/temphum_relayctl/src/main.cpp b/pio/temphum_relayctl/src/main.cpp index 7f0945e..7f0945e 100644 --- a/platformio/temphum_relayctl/src/main.cpp +++ b/pio/temphum_relayctl/src/main.cpp diff --git a/platformio/common/libs/main/library.json b/platformio/common/libs/main/library.json deleted file mode 100644 index 728d4f8..0000000 --- a/platformio/common/libs/main/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "homekit_main", - "version": "1.0.10", - "build": { - "flags": "-I../../include" - }, - "dependencies": { - "homekit_mqtt_module_ota": "file://../common/libs/mqtt_module_ota", - "homekit_mqtt_module_diagnostics": "file://../common/libs/mqtt_module_diagnostics" - } -} - diff --git a/platformio/common/libs/mqtt_module_ota/library.json b/platformio/common/libs/mqtt_module_ota/library.json deleted file mode 100644 index 4f40a47..0000000 --- a/platformio/common/libs/mqtt_module_ota/library.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "homekit_mqtt_module_ota", - "version": "1.0.5", - "build": { - "flags": "-I../../include" - }, - "dependencies": { - "homekit_led": "file://../common/libs/led", - "homekit_mqtt": "file://../common/libs/mqtt" - } -} diff --git a/platformio/common/libs/mqtt_module_relay/library.json b/platformio/common/libs/mqtt_module_relay/library.json deleted file mode 100644 index 6cbbfb0..0000000 --- a/platformio/common/libs/mqtt_module_relay/library.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "homekit_mqtt_module_relay", - "version": "1.0.5", - "build": { - "flags": "-I../../include" - }, - "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt", - "homekit_relay": "file://../common/libs/relay" - } -} diff --git a/platformio/dumb_mqtt/.gitignore b/platformio/dumb_mqtt/.gitignore deleted file mode 100644 index 3fe18ad..0000000 --- a/platformio/dumb_mqtt/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.pio -CMakeListsPrivate.txt -cmake-build-*/ diff --git a/platformio/relayctl/.gitignore b/platformio/relayctl/.gitignore deleted file mode 100644 index 3fe18ad..0000000 --- a/platformio/relayctl/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.pio -CMakeListsPrivate.txt -cmake-build-*/ diff --git a/platformio/temphum/.gitignore b/platformio/temphum/.gitignore deleted file mode 100644 index 3fe18ad..0000000 --- a/platformio/temphum/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.pio -CMakeListsPrivate.txt -cmake-build-*/ diff --git a/requirements.txt b/requirements.txt index 893cdfc..fbe57a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,23 +2,23 @@ paho-mqtt==1.6.1 inverterd~=1.0.3 clickhouse-driver~=0.2.0 mysql-connector-python~=8.0.27 -Werkzeug==2.2.2 +Werkzeug==2.3.6 uwsgi~=2.0.20 -python-telegram-bot==13.15 -requests==2.28.1 -aiohttp~=3.8.1 -pytz==2022.6 +python-telegram-bot==20.3 +requests==2.31.0 +aiohttp~=3.9.1 +pytz==2023.3 PyYAML~=6.0 -apscheduler~=3.9.1 +apscheduler==3.10.1 psutil~=5.9.1 aioshutil~=1.1 -scikit-image~=0.19.3 +scikit-image==0.21.0 cerberus~=1.3.4 +phonenumbers~=8.13.28 # following can be installed from debian repositories # matplotlib~=3.5.0 -Pillow~=9.1.1 +Pillow==9.5.0 -# for polaris kettle protocol implementation -cryptography==38.0.4 -zeroconf==0.39.4
\ No newline at end of file +jinja2~=3.1.2 +aiohttp-jinja2~=1.5.1 diff --git a/requirements_kettle.txt b/requirements_kettle.txt new file mode 100644 index 0000000..d003269 --- /dev/null +++ b/requirements_kettle.txt @@ -0,0 +1,3 @@ +# for polaris kettle protocol implementation +cryptography==41.0.1 +zeroconf==0.64.1
\ No newline at end of file diff --git a/src/gpiorelayd.py b/src/gpiorelayd.py deleted file mode 100755 index f1a9e57..0000000 --- a/src/gpiorelayd.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -import logging -import os -import sys - -from home.config import config -from home.relay.sunxi_h3_server import RelayServer - -logger = logging.getLogger(__name__) - - -if __name__ == '__main__': - if not os.getegid() == 0: - sys.exit('Must be run as root.') - - config.load_app() - - try: - s = RelayServer(pinname=config.get('relayd.pin'), - addr=config.get_addr('relayd.listen')) - s.run() - except KeyboardInterrupt: - logger.info('Exiting...') diff --git a/src/home/api/__init__.py b/src/home/api/__init__.py deleted file mode 100644 index 782a61e..0000000 --- a/src/home/api/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -import importlib - -__all__ = ['WebAPIClient', 'RequestParams'] - - -def __getattr__(name): - if name in __all__: - module = importlib.import_module(f'.web_api_client', __name__) - return getattr(module, name) - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/home/api/__init__.pyi b/src/home/api/__init__.pyi deleted file mode 100644 index 1b812d6..0000000 --- a/src/home/api/__init__.pyi +++ /dev/null @@ -1,4 +0,0 @@ -from .web_api_client import ( - RequestParams as RequestParams, - WebAPIClient as WebAPIClient -) diff --git a/src/home/camera/__init__.py b/src/home/camera/__init__.py deleted file mode 100644 index 626930b..0000000 --- a/src/home/camera/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .types import CameraType
\ No newline at end of file diff --git a/src/home/camera/types.py b/src/home/camera/types.py deleted file mode 100644 index de59022..0000000 --- a/src/home/camera/types.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import Enum - - -class CameraType(Enum): - ESP32 = 'esp32' diff --git a/src/home/http/__init__.py b/src/home/http/__init__.py deleted file mode 100644 index 6030e95..0000000 --- a/src/home/http/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .http import serve, ok, routes, HTTPServer -from aiohttp.web import FileResponse, StreamResponse, Request, Response diff --git a/src/home/inverter/config.py b/src/home/inverter/config.py deleted file mode 100644 index 62b8859..0000000 --- a/src/home/inverter/config.py +++ /dev/null @@ -1,13 +0,0 @@ -from ..config import ConfigUnit -from typing import Optional - - -class InverterdConfig(ConfigUnit): - NAME = 'inverterd' - - @staticmethod - def schema() -> Optional[dict]: - return { - 'remote_addr': {'type': 'string'}, - 'local_addr': {'type': 'string'}, - }
\ No newline at end of file diff --git a/src/openwrt_log_analyzer.py b/src/openwrt_log_analyzer.py deleted file mode 100755 index 35b755f..0000000 --- a/src/openwrt_log_analyzer.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -import home.telegram as telegram - -from home.config import config -from home.database import BotsDatabase, SimpleState - -""" -config.toml example: - -[simple_state] -file = "/home/user/.config/openwrt_log_analyzer/state.txt" - -[mysql] -host = "localhost" -database = ".." -user = ".." -password = ".." - -[devices] -Device1 = "00:00:00:00:00:00" -Device2 = "01:01:01:01:01:01" - -[telegram] -chat_id = ".." -token = ".." -parse_mode = "HTML" - -[openwrt_log_analyzer] -limit = 10 -""" - - -def main(mac: str, - title: str, - ap: int) -> int: - db = BotsDatabase() - - data = db.get_openwrt_logs(filter_text=mac, - min_id=state['last_id'], - access_point=ap, - limit=config['openwrt_log_analyzer']['limit']) - if not data: - return 0 - - max_id = 0 - for log in data: - if log.id > max_id: - max_id = log.id - - text = '\n'.join(map(lambda s: str(s), data)) - telegram.send_message(f'<b>{title} (AP #{ap})</b>\n\n' + text) - - return max_id - - -if __name__ == '__main__': - config.load_app('openwrt_log_analyzer') - for ap in config['openwrt_log_analyzer']['aps']: - state_file = config['simple_state']['file'] - state_file = state_file.replace('.txt', f'-{ap}.txt') - - state = SimpleState(file=state_file, - default={'last_id': 0}) - - max_last_id = 0 - for name, mac in config['devices'].items(): - last_id = main(mac, title=name, ap=ap) - if last_id > max_last_id: - max_last_id = last_id - - if max_last_id: - state['last_id'] = max_last_id diff --git a/src/relay_mqtt_http_proxy.py b/src/relay_mqtt_http_proxy.py deleted file mode 100755 index 50a74a1..0000000 --- a/src/relay_mqtt_http_proxy.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -from home import http -from home.config import config -from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule -from home.mqtt.module.relay import MqttRelayState, MqttRelayModule -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -from typing import Optional, Union - -mqtt: Optional[MqttWrapper] = None -mqtt_nodes: dict[str, MqttNode] = {} -relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {} -relay_states: dict[str, MqttRelayState] = {} - - -def on_mqtt_message(node: MqttNode, - message: MqttPayload): - if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): - kwargs = dict(rssi=message.rssi, enabled=message.flags.state) - if device_id not in relay_states: - relay_states[device_id] = MqttRelayState() - relay_states[device_id].update(**kwargs) - - -class RelayMqttHttpProxy(http.HTTPServer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.get('/relay/{id}/on', self.relay_on) - self.get('/relay/{id}/off', self.relay_off) - self.get('/relay/{id}/toggle', self.relay_toggle) - - async def _relay_on_off(self, - enable: Optional[bool], - req: http.Request): - node_id = req.match_info['id'] - node_secret = req.query['secret'] - - node = mqtt_nodes[node_id] - relay_module = relay_modules[node_id] - - if enable is None: - if node_id in relay_states and relay_states[node_id].ever_updated: - cur_state = relay_states[node_id].enabled - else: - cur_state = False - enable = not cur_state - - if not node.secret: - node.secret = node_secret - relay_module.switchpower(enable) - return self.ok() - - async def relay_on(self, req: http.Request): - return await self._relay_on_off(True, req) - - async def relay_off(self, req: http.Request): - return await self._relay_on_off(False, req) - - async def relay_toggle(self, req: http.Request): - return await self._relay_on_off(None, req) - - -if __name__ == '__main__': - config.load_app('relay_mqtt_http_proxy') - - mqtt = MqttWrapper() - for device_id, data in config['relays'].items(): - mqtt_node = MqttNode(node_id=device_id) - relay_modules[device_id] = mqtt_node.load_module('relay') - mqtt_nodes[device_id] = mqtt_node - mqtt_node.add_payload_callback(on_mqtt_message) - mqtt.add_node(mqtt_node) - mqtt_node.add_payload_callback(on_mqtt_message) - - mqtt.configure_tls() - mqtt.connect_and_loop(loop_forever=False) - - proxy = RelayMqttHttpProxy(config.get_addr('server.listen')) - try: - proxy.run() - except KeyboardInterrupt: - mqtt.disconnect() diff --git a/src/test_new_config.py b/src/test_new_config.py deleted file mode 100755 index ae89495..0000000 --- a/src/test_new_config.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -from home.config import config -from home.mqtt import MqttNodesConfig -from pprint import pprint - - -if __name__ == '__main__': - config.load_app(name=False) - - c = MqttNodesConfig() - pprint(c.get_nodes(filters=('temphum',), only_names=False))
\ No newline at end of file diff --git a/systemd/camera_node.service b/systemd/camera_node.service index 0de3cc1..83471bd 100644 --- a/systemd/camera_node.service +++ b/systemd/camera_node.service @@ -6,7 +6,7 @@ After=network-online.target User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/camera_node.py +ExecStart=/home/user/homekit/bin/camera_node.py WorkingDirectory=/home/user [Install] diff --git a/systemd/camera_node@.service b/systemd/camera_node@.service index 414881e..a272002 100644 --- a/systemd/camera_node@.service +++ b/systemd/camera_node@.service @@ -6,7 +6,7 @@ After=network-online.target User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/camera_node.py --config /home/user/.config/camera_node.%i.yaml +ExecStart=/home/user/homekit/bin/camera_node.py --config /home/user/.config/camera_node.%i.yaml WorkingDirectory=/home/user [Install] diff --git a/systemd/esp32cam_capture_diff_node.service b/systemd/esp32cam_capture_diff_node.service index ecc4861..a742edc 100644 --- a/systemd/esp32cam_capture_diff_node.service +++ b/systemd/esp32cam_capture_diff_node.service @@ -6,7 +6,7 @@ After=network-online.target User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/esp32cam_capture_diff_node.py +ExecStart=/home/user/homekit/bin/esp32cam_capture_diff_node.py WorkingDirectory=/home/user [Install] diff --git a/systemd/gpiorelayd@.service b/systemd/gpiorelayd@.service index 0cc0582..e3922dc 100644 --- a/systemd/gpiorelayd@.service +++ b/systemd/gpiorelayd@.service @@ -1,12 +1,13 @@ [Unit] -Description=GPIO Relay Daemon +Description=Homekit: GPIO Relay Daemon for H3 boards After=network-online.target [Service] User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/gpiorelayd.py -c /etc/gpiorelayd.conf.d/%i.toml +EnvironmentFile=/etc/default/homekit_gpiorelayd_%i +ExecStart=/home/user/homekit/bin/gpiorelayd.py --pin $PIN --listen $LISTEN WorkingDirectory=/root [Install] diff --git a/systemd/inverter_bot.service b/systemd/inverter_bot.service index 96612ae..c5d4aec 100644 --- a/systemd/inverter_bot.service +++ b/systemd/inverter_bot.service @@ -6,7 +6,7 @@ After=inverterd.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/inverter_bot.py +ExecStart=/home/user/homekit/bin/inverter_bot.py WorkingDirectory=/home/user [Install] diff --git a/systemd/inverter_mqtt_receiver.service b/systemd/inverter_mqtt_receiver.service index fedf11f..eac8442 100644 --- a/systemd/inverter_mqtt_receiver.service +++ b/systemd/inverter_mqtt_receiver.service @@ -6,8 +6,8 @@ After=clickhouse-server.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/inverter_mqtt_util.py receiver +ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py receiver WorkingDirectory=/home/user [Install] -WantedBy=multi-user.target
\ No newline at end of file +WantedBy=multi-user.target diff --git a/systemd/inverter_mqtt_sender.service b/systemd/inverter_mqtt_sender.service index 34272bb..4340912 100644 --- a/systemd/inverter_mqtt_sender.service +++ b/systemd/inverter_mqtt_sender.service @@ -6,8 +6,8 @@ After=inverterd.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/inverter_mqtt_util.py sender +ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py sender WorkingDirectory=/home/user [Install] -WantedBy=multi-user.target
\ No newline at end of file +WantedBy=multi-user.target diff --git a/systemd/ipcam_capture@.service b/systemd/ipcam_capture@.service deleted file mode 100644 index b1c363e..0000000 --- a/systemd/ipcam_capture@.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=save ipcam streams -After=network-online.target - -[Service] -Restart=always -RestartSec=3 -User=user -Group=user -EnvironmentFile=/etc/ipcam_capture.conf.d/%i.conf -ExecStart=/home/user/homekit/tools/ipcam_capture.sh --outdir $OUTDIR --creds $CREDS --ip $IP --port $PORT $ARGS -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/systemd/ipcam_rtsp2hls@.service b/systemd/ipcam_rtsp2hls@.service deleted file mode 100644 index efcdd6a..0000000 --- a/systemd/ipcam_rtsp2hls@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=convert rtsp to hls for viewing live camera feeds in browser -After=network-online.target - -[Service] -Restart=always -RestartSec=3 -User=user -Group=user -EnvironmentFile=/etc/ipcam_rtsp2hls.conf.d/%i.conf -ExecStart=/home/user/homekit/tools/ipcam_rtsp2hls.sh --name %i --user $USER --password $PASSWORD --ip $IP --port $PORT $ARGS -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/systemd/ipcam_server.service b/systemd/ipcam_server.service index 07ac95f..53e588d 100644 --- a/systemd/ipcam_server.service +++ b/systemd/ipcam_server.service @@ -1,5 +1,5 @@ [Unit] -Description=HomeKit IPCam Server +Description=Homekit IPCam Server After=network-online.target [Service] @@ -7,7 +7,8 @@ User=user Group=user Restart=always RestartSec=10 -ExecStart=/home/user/homekit/src/ipcam_server.py +EnvironmentFile=/etc/default/homekit_ipcam_server +ExecStart=/home/user/homekit/bin/ipcam_server.py --listen "$LISTEN" --database-path "$DATABASE_PATH" WorkingDirectory=/home/user [Install] diff --git a/systemd/polaris_kettle_bot.service b/systemd/polaris_kettle_bot.service index f91ed60..86bb293 100644 --- a/systemd/polaris_kettle_bot.service +++ b/systemd/polaris_kettle_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/polaris_kettle_bot.py +ExecStart=/home/user/homekit/bin/polaris_kettle_bot.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/pump_bot.service b/systemd/pump_bot.service index dd8a46b..b59f5b9 100644 --- a/systemd/pump_bot.service +++ b/systemd/pump_bot.service @@ -6,7 +6,7 @@ After=gpiorelayd.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/pump_bot.py +ExecStart=/home/user/homekit/bin/pump_bot.py WorkingDirectory=/home/user [Install] diff --git a/systemd/pump_mqtt_bot.service b/systemd/pump_mqtt_bot.service index 95f9419..6c72cbf 100644 --- a/systemd/pump_mqtt_bot.service +++ b/systemd/pump_mqtt_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/pump_mqtt_bot.py +ExecStart=/home/user/homekit/bin/pump_mqtt_bot.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/relay_mqtt_bot.service b/systemd/relay_mqtt_bot.service index 93696ac..3bac158 100644 --- a/systemd/relay_mqtt_bot.service +++ b/systemd/relay_mqtt_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/relay_mqtt_bot.py +ExecStart=/home/user/homekit/bin/relay_mqtt_bot.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/relay_mqtt_http_proxy.service b/systemd/relay_mqtt_http_proxy.service index 316a920..8301d52 100644 --- a/systemd/relay_mqtt_http_proxy.service +++ b/systemd/relay_mqtt_http_proxy.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/relay_mqtt_http_proxy.py +ExecStart=/home/user/homekit/bin/relay_mqtt_http_proxy.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/sensors_bot.service b/systemd/sensors_bot.service index 50128b3..2470d92 100644 --- a/systemd/sensors_bot.service +++ b/systemd/sensors_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/sensors_bot.py +ExecStart=/home/user/homekit/bin/sensors_bot.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/sound_bot.service b/systemd/sound_bot.service index 51a9e0f..e0b5500 100644 --- a/systemd/sound_bot.service +++ b/systemd/sound_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/sound_bot.py +ExecStart=/home/user/homekit/bin/sound_bot.py [Install] WantedBy=multi-user.target
\ No newline at end of file diff --git a/systemd/sound_node.service b/systemd/sound_node.service index e3e3afd..a14ec1f 100644 --- a/systemd/sound_node.service +++ b/systemd/sound_node.service @@ -6,7 +6,7 @@ After=network-online.target User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/sound_node.py --config /etc/sound_node.toml +ExecStart=/home/user/homekit/bin/sound_node.py --config /etc/sound_node.toml WorkingDirectory=/root [Install] diff --git a/systemd/sound_sensor_node.service b/systemd/sound_sensor_node.service index d10f976..dfc2ecd 100644 --- a/systemd/sound_sensor_node.service +++ b/systemd/sound_sensor_node.service @@ -6,7 +6,7 @@ After=network-online.target User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/sound_sensor_node.py --config /etc/sound_sensor_node.toml +ExecStart=/home/user/homekit/bin/sound_sensor_node.py --config /etc/sound_sensor_node.toml WorkingDirectory=/root [Install] diff --git a/systemd/sound_sensor_server.service b/systemd/sound_sensor_server.service index 0133e53..5ab08cd 100644 --- a/systemd/sound_sensor_server.service +++ b/systemd/sound_sensor_server.service @@ -6,7 +6,7 @@ After=network-online.target User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/sound_sensor_server.py +ExecStart=/home/user/homekit/bin/sound_sensor_server.py WorkingDirectory=/home/user [Install] diff --git a/systemd/temphumd.service b/systemd/temphumd.service index 1da9617..dd5ec55 100644 --- a/systemd/temphumd.service +++ b/systemd/temphumd.service @@ -4,7 +4,7 @@ After=network-online.target [Service] Restart=on-failure -ExecStart=/home/user/homekit/src/temphumd.py --config /etc/temphumd.toml +ExecStart=/home/user/homekit/bin/temphumd.py --config /etc/temphumd.toml [Install] WantedBy=multi-user.target diff --git a/systemd/temphumd@.service b/systemd/temphumd@.service index d1c840d..7b1b11e 100644 --- a/systemd/temphumd@.service +++ b/systemd/temphumd@.service @@ -4,7 +4,7 @@ After=network-online.target [Service] Restart=on-failure -ExecStart=/home/user/homekit/src/temphumd.py --config /etc/temphumd-%i.toml +ExecStart=/home/user/homekit/bin/temphumd.py --config /etc/temphumd-%i.toml [Install] WantedBy=multi-user.target diff --git a/tasks/df_h.sh b/tasks/df_h.sh new file mode 100644 index 0000000..eaa10fe --- /dev/null +++ b/tasks/df_h.sh @@ -0,0 +1,2 @@ +#!/bin/sh +df -h
\ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/test/__init__.py +++ /dev/null diff --git a/test/__py_include.py b/test/__py_include.py new file mode 100644 index 0000000..8f98830 --- /dev/null +++ b/test/__py_include.py @@ -0,0 +1,9 @@ +import sys +import os.path + +for _name in ('include/py',): + sys.path.extend([ + os.path.realpath( + os.path.join(os.path.dirname(os.path.join(__file__)), '..', _name) + ) + ])
\ No newline at end of file diff --git a/test/mqtt_relay_server_util.py b/test/mqtt_relay_server_util.py index 35bbf02..6c02d75 100755 --- a/test/mqtt_relay_server_util.py +++ b/test/mqtt_relay_server_util.py @@ -1,18 +1,11 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include -from src.home.config import config -from src.home.mqtt.relay import MQTTRelayClient +from homekit.config import config if __name__ == '__main__': - config.load_app('test_mqtt_relay_server') - relay = MQTTRelayClient('test') - relay.configure_tls() - relay.connect_and_loop() + print(config) + # config.load_app('test_mqtt_relay_server') + # relay = MQTTRelayClient('test') + # relay.connect_and_loop() diff --git a/test/mqtt_relay_util.py b/test/mqtt_relay_util.py index 3634bbe..efa5588 100755 --- a/test/mqtt_relay_util.py +++ b/test/mqtt_relay_util.py @@ -1,15 +1,9 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from argparse import ArgumentParser -from src.home.config import config -from src.home.mqtt.relay import MQTTRelayController +from homekit.config import config +from homekit.mqtt.relay import MQTTRelayController if __name__ == '__main__': @@ -22,7 +16,6 @@ if __name__ == '__main__': arg = parser.parse_args() relay = MQTTRelayController('test') - relay.configure_tls() relay.connect_and_loop(loop_forever=False) if arg.on: @@ -36,4 +29,4 @@ if __name__ == '__main__': state=False, signal=-59, fw_v=1.0 - ))
\ No newline at end of file + )) diff --git a/test/test.py b/test/test.py deleted file mode 100755 index 7ea37e6..0000000 --- a/test/test.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python -from home.relay import RelayClient - - -if __name__ == '__main__': - c = RelayClient() - print(c, c._host)
\ No newline at end of file diff --git a/test/test_amixer.py b/test/test_amixer.py index 464941e..e4abc73 100755 --- a/test/test_amixer.py +++ b/test/test_amixer.py @@ -1,12 +1,9 @@ #!/usr/bin/env python3 -import sys, os.path -sys.path.extend([ - os.path.realpath(os.path.join(os.path.dirname(os.path.join(__file__)), '..')), -]) +import __py_include from argparse import ArgumentParser -from src.home.config import config -from src.home.audio import amixer +from homekit.config import config +from homekit.audio import amixer def validate_control(input: str): diff --git a/test/test_api.py b/test/test_api.py index e80eb4c..b35a597 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,19 +1,12 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include -from src.home.api import WebAPIClient -from src.home.api.types import BotType -from src.home.config import config +from homekit.api import WebApiClient +from homekit.config import config if __name__ == '__main__': config.load_app('test_api') - api = WebAPIClient() - print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) + # api = WebApiClient() + # print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) diff --git a/test/test_esp32_cam.py b/test/test_esp32_cam.py index d743f09..962768f 100755 --- a/test/test_esp32_cam.py +++ b/test/test_esp32_cam.py @@ -1,18 +1,12 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from pprint import pprint from argparse import ArgumentParser from time import sleep -from src.home.util import parse_addr -from src.home.camera import esp32 -from src.home.config import config +from homekit.util import Addr +from homekit.camera import esp32 +from homekit.config import config if __name__ == '__main__': parser = ArgumentParser() @@ -22,7 +16,7 @@ if __name__ == '__main__': help='print status and exit') arg = config.load_app(False, parser=parser) - cam = esp32.WebClient(addr=parse_addr(arg.addr)) + cam = esp32.WebClient(addr=Addr.fromstring(arg.addr)) if arg.status: status = cam.getstatus() diff --git a/test/test_inverter_monitor.py b/test/test_inverter_monitor.py index 621c0e9..3231bab 100755 --- a/test/test_inverter_monitor.py +++ b/test/test_inverter_monitor.py @@ -1,22 +1,11 @@ #!/usr/bin/env python3 -import cmd -import time -import logging -import socket -import sys -import threading -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from enum import Enum, auto from typing import Optional -from src.home.util import stringify -from src.home.config import config -from src.home.inverter import ( +from homekit.util import stringify +from homekit.config import config +from homekit.inverter import ( wrapper_instance as inverter, InverterMonitor, diff --git a/test/test_ipcam_server_cleanup.py b/test/test_ipcam_server_cleanup.py index 5f313a4..ae8d31c 100644 --- a/test/test_ipcam_server_cleanup.py +++ b/test/test_ipcam_server_cleanup.py @@ -1,19 +1,13 @@ #!/usr/bin/env python3 -import shutil -import sys +import __py_include +import logging import os +import shutil import re -import logging -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) from functools import cmp_to_key from datetime import datetime -from pprint import pprint -from src.home.config import config +from homekit.config import config logger = logging.getLogger(__name__) diff --git a/test/test_modems.py b/test/test_modems.py new file mode 100755 index 0000000..39981f7 --- /dev/null +++ b/test/test_modems.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import __py_include +from homekit.modem import E3372, ModemsConfig + + +if __name__ == '__main__': + mc = ModemsConfig() + modem = mc.get('mts-azov') + cl = E3372(modem['ip'], legacy_token_auth=modem['legacy_auth']) diff --git a/test/test_polaris_stuff.py b/test/test_polaris_stuff.py index b921891..7778667 100755 --- a/test/test_polaris_stuff.py +++ b/test/test_polaris_stuff.py @@ -1,13 +1,6 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) - -import src.syncleo as polaris +import __py_include +import syncleo if __name__ == '__main__': diff --git a/test/test_record_upload.py b/test/test_record_upload.py index 21e3d68..f9c83d8 100755 --- a/test/test_record_upload.py +++ b/test/test_record_upload.py @@ -1,19 +1,12 @@ #!/usr/bin/env python3 +import __py_include import logging -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) - import time -from src.home.api import WebAPIClient, RequestParams -from src.home.config import config -from src.home.media import SoundRecordClient -from src.home.util import parse_addr +from homekit.api import WebApiClient, RequestParams +from homekit.config import config +from homekit.media import SoundRecordClient +from homekit.util import Addr logger = logging.getLogger(__name__) @@ -68,13 +61,13 @@ if __name__ == '__main__': nodes = {} for name, addr in config['nodes'].items(): - nodes[name] = parse_addr(addr) + nodes[name] = Addr(addr) record = SoundRecordClient(nodes, error_handler=record_error, finished_handler=record_finished, download_on_finish=True) - api = WebAPIClient() + api = WebApiClient() api.enable_async(error_handler=api_error_handler, success_handler=api_success_handler) diff --git a/test/test_send_fake_sound_hit.py b/test/test_send_fake_sound_hit.py index 9660c45..3cc3e50 100755 --- a/test/test_send_fake_sound_hit.py +++ b/test/test_send_fake_sound_hit.py @@ -1,14 +1,8 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from argparse import ArgumentParser -from src.home.util import send_datagram, stringify, parse_addr +from homekit.util import send_datagram, stringify, Addr if __name__ == '__main__': @@ -22,4 +16,4 @@ if __name__ == '__main__': args = parser.parse_args() - send_datagram(stringify([args.name, args.hits]), parse_addr(args.server)) + send_datagram(stringify([args.name, args.hits]), Addr.fromstring(args.server)) diff --git a/test/test_sensors_plot.py b/test/test_sensors_plot.py deleted file mode 100755 index e69de29..0000000 --- a/test/test_sensors_plot.py +++ /dev/null diff --git a/test/test_sound_node_client.py b/test/test_sound_node_client.py index 16feb78..c3748ca 100755 --- a/test/test_sound_node_client.py +++ b/test/test_sound_node_client.py @@ -1,11 +1,8 @@ #!/usr/bin/env python3 -import sys, os.path -sys.path.extend([ - os.path.realpath(os.path.join(os.path.dirname(os.path.join(__file__)), '..')), -]) +import __py_include -from src.home.api.errors import ApiResponseError -from src.home.media import SoundNodeClient +from homekit.api.errors import ApiResponseError +from homekit.media import SoundNodeClient if __name__ == '__main__': diff --git a/test/test_sound_server_api.py b/test/test_sound_server_api.py index 5295a5d..11cd422 100755 --- a/test/test_sound_server_api.py +++ b/test/test_sound_server_api.py @@ -1,17 +1,11 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include import threading from time import sleep -from src.home.config import config -from src.home.api import WebAPIClient -from src.home.api.types import SoundSensorLocation +from homekit.config import config +from homekit.api import WebApiClient +from homekit.api.types import SoundSensorLocation from typing import List, Tuple interrupted = False @@ -59,7 +53,7 @@ if __name__ == '__main__': config.load_app('test_api') hc = HitCounter() - api = WebAPIClient() + api = WebApiClient() hc.add('spb1', 1) # hc.add('big_house', 123) diff --git a/test/test_stopwatch.py b/test/test_stopwatch.py index 6ff2c0e..1da0fe7 100755 --- a/test/test_stopwatch.py +++ b/test/test_stopwatch.py @@ -1,4 +1,5 @@ -from home.util import Stopwatch, StopwatchError +import __py_include +from homekit.util import Stopwatch, StopwatchError from time import sleep diff --git a/test/test_telegram_aio_send_photo.py b/test/test_telegram_aio_send_photo.py index 4d05c03..019fa92 100644 --- a/test/test_telegram_aio_send_photo.py +++ b/test/test_telegram_aio_send_photo.py @@ -1,16 +1,9 @@ #!/usr/bin/env python3 +import __py_include import asyncio -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import homekit.telegram.aio as telegram -import src.home.telegram.aio as telegram - -from src.home.config import config +from homekit.config import config async def main(): diff --git a/tools/ipcam_capture.sh b/tools/ipcam_capture.sh deleted file mode 100755 index 08b9093..0000000 --- a/tools/ipcam_capture.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash - -PROGNAME="$0" -PORT=554 -IP= -CREDS= -DEBUG=0 -CHANNEL=1 -FORCE_UDP=0 -FORCE_TCP=0 -EXTENSION="mp4" - -die() { - echo >&2 "error: $@" - exit 1 -} - -usage() { - cat <<EOF -usage: $PROGNAME [OPTIONS] COMMAND - -Options: - --outdir output directory - --ip camera IP - --port RTSP port (default: 554) - --creds - --debug - --force-tcp - --force-udp - --channel 1|2 - -EOF - exit -} - -validate_channel() { - local c="$1" - case "$c" in - 1 | 2) - : - ;; - *) - die "Invalid channel" - ;; - esac -} - -[ -z "$1" ] && usage - -while [[ $# -gt 0 ]]; do - case "$1" in - --ip | --port | --creds | --outdir) - _var=${1:2} - _var=${_var^^} - printf -v "$_var" '%s' "$2" - shift - ;; - - --debug) - DEBUG=1 - ;; - - --force-tcp) - FORCE_TCP=1 - ;; - - --force-udp) - FORCE_UDP=1 - ;; - - --channel) - CHANNEL="$2" - shift - ;; - - --mov) - EXTENSION="mov" - ;; - - --mpv) - EXTENSION="mpv" - ;; - - *) - die "Unrecognized argument: $1" - ;; - esac - shift -done - -[ -z "$OUTDIR" ] && die "You must specify output directory (--outdir)." -[ -z "$IP" ] && die "You must specify camera IP address (--ip)." -[ -z "$PORT" ] && die "Port can't be empty." -[ -z "$CREDS" ] && die "You must specify credentials (--creds)." -validate_channel "$CHANNEL" - -if [ ! -d "${OUTDIR}" ]; then - mkdir "${OUTDIR}" || die "Failed to create ${OUTDIR}/${NAME}!" - echo "Created $OUTDIR." -fi - -args= -if [ "$DEBUG" = "1" ]; then - args="$args -v info" -else - args="$args -nostats -loglevel warning" -fi - -if [ "$FORCE_TCP" = "1" ]; then - args="$args -rtsp_transport tcp" -elif [ "$FORCE_UDP" = "1" ]; then - args="$args -rtsp_transport udp" -fi - -[ ! -z "$CREDS" ] && CREDS="${CREDS}@" - -ffmpeg $args -i rtsp://${CREDS}${IP}:${PORT}/Streaming/Channels/${CHANNEL} \ - -c copy -f segment -strftime 1 -segment_time 00:10:00 -segment_atclocktime 1 \ - "$OUTDIR/record_%Y-%m-%d-%H.%M.%S.${EXTENSION}" diff --git a/tools/ipcam_rtsp2hls.sh b/tools/ipcam_rtsp2hls.sh deleted file mode 100755 index c321820..0000000 --- a/tools/ipcam_rtsp2hls.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash - -PROGNAME="$0" -OUTDIR=/var/ipcamfs # should be tmpfs -PORT=554 -NAME= -IP= -USER= -PASSWORD= -DEBUG=0 -CHANNEL=1 -FORCE_UDP=0 -FORCE_TCP=0 -CUSTOM_PATH= - -die() { - echo >&2 "error: $@" - exit 1 -} - -usage() { - cat <<EOF -usage: $PROGNAME [OPTIONS] COMMAND - -Options: - --ip camera IP - --port RTSP port (default: 554) - --name camera name (chunks will be stored under $OUTDIR/{name}/) - --user - --password - --debug - --force-tcp - --force-udp - --channel 1|2 - --custom-path PATH - -EOF - exit -} - -validate_channel() { - local c="$1" - case "$c" in - 1|2) - : - ;; - *) - die "Invalid channel" - ;; - esac -} - -[ -z "$1" ] && usage - -while [[ $# -gt 0 ]]; do - case "$1" in - --ip|--port|--name|--user|--password) - _var=${1:2} - _var=${_var^^} - printf -v "$_var" '%s' "$2" - shift - ;; - - --debug) - DEBUG=1 - ;; - - --force-tcp) - FORCE_TCP=1 - ;; - - --force-udp) - FORCE_UDP=1 - ;; - - --channel) - CHANNEL="$2" - shift - ;; - - --custom-path) - CUSTOM_PATH="$2" - shift - ;; - - *) - die "Unrecognized argument: $1" - ;; - esac - shift -done - -[ -z "$IP" ] && die "You must specify camera IP address (--ip)." -[ -z "$PORT" ] && die "Port can't be empty." -[ -z "$NAME" ] && die "You must specify camera name (--name)." -[ -z "$USER" ] && die "You must specify username (--user)." -[ -z "$PASSWORD" ] && die "You must specify username (--password)." -validate_channel "$CHANNEL" - -if [ ! -d "${OUTDIR}/${NAME}" ]; then - mkdir "${OUTDIR}/${NAME}" || die "Failed to create ${OUTDIR}/${NAME}!" -fi - -args= -if [ "$DEBUG" = "1" ]; then - args="-v info" -else - args="-nostats -loglevel error" -fi - -if [ "$FORCE_TCP" = "1" ]; then - args="$args -rtsp_transport tcp" -elif [ "$FORCE_UDP" = "1" ]; then - args="$args -rtsp_transport udp" -fi - -if [ -z "$CUSTOM_PATH" ]; then - path="/Streaming/Channels/${CHANNEL}" -else - path="$CUSTOM_PATH" -fi - -ffmpeg $args -i "rtsp://${USER}:${PASSWORD}@${IP}:${PORT}${path}" \ - -c:v copy -c:a copy -bufsize 1835k \ - -pix_fmt yuv420p \ - -flags -global_header -hls_time 2 -hls_list_size 3 -hls_flags delete_segments \ - ${OUTDIR}/${NAME}/live.m3u8 diff --git a/tools/process-motion-timecodes.py b/tools/process-motion-timecodes.py deleted file mode 100755 index 7be7977..0000000 --- a/tools/process-motion-timecodes.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -import os.path -from src.home.camera.util import dvr_scan_timecodes - -from argparse import ArgumentParser -from datetime import datetime, timedelta - -DATETIME_FORMAT = '%Y-%m-%d-%H.%M.%S' - - -def chunks(lst, n): - for i in range(0, len(lst), n): - yield lst[i:i + n] - - -def time2seconds(time: str) -> int: - time, frac = time.split('.') - frac = int(frac) - - h, m, s = [int(i) for i in time.split(':')] - - return round(s + m*60 + h*3600 + frac/1000) - - -def filename_to_datetime(filename: str) -> datetime: - filename = os.path.basename(filename).replace('record_', '').replace('.mp4', '') - return datetime.strptime(filename, DATETIME_FORMAT) - - -if __name__ == '__main__': - parser = ArgumentParser() - parser.add_argument('--source-filename', type=str, required=True, - help='recording filename') - parser.add_argument('--timecodes', type=str, required=True, - help='timecodes') - parser.add_argument('--padding', type=int, default=2, - help='amount of seconds to add before and after each fragment') - arg = parser.parse_args() - - if arg.padding < 0: - raise ValueError('invalid padding') - - fragments = dvr_scan_timecodes(arg.timecodes) - file_dt = filename_to_datetime(arg.source_filename) - - for fragment in fragments: - start, end = fragment - - start -= arg.padding - end += arg.padding - - if start < 0: - start = 0 - - duration = end - start - - dt1 = (file_dt + timedelta(seconds=start)).strftime(DATETIME_FORMAT) - dt2 = (file_dt + timedelta(seconds=end)).strftime(DATETIME_FORMAT) - filename = f'{dt1}__{dt2}.mp4' - - print(f'{start} {duration} {filename}') diff --git a/tools/rotate-video.sh b/tools/rotate-video.sh index 6d27b44..5ce4efe 100755 --- a/tools/rotate-video.sh +++ b/tools/rotate-video.sh @@ -5,7 +5,7 @@ set -e DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &>/dev/null && pwd )" PROGNAME="$0" -. "$DIR/lib.bash" +. "$DIR/../include/bash/include.bash" usage() { diff --git a/tools/video-util.sh b/tools/video-util.sh index 0ee5560..6fe6109 100755 --- a/tools/video-util.sh +++ b/tools/video-util.sh @@ -5,7 +5,7 @@ set -e DIR="$( cd "$( dirname "$(realpath "${BASH_SOURCE[0]}")" )" &> /dev/null && pwd )" PROGNAME="$0" -. "$DIR/lib.bash" +. "$DIR/../include/bash/include.bash" input= output= diff --git a/localwebsite/htdocs/assets/app.css b/web/kbn_assets/app.css index 3146bcf..1a4697a 100644 --- a/localwebsite/htdocs/assets/app.css +++ b/web/kbn_assets/app.css @@ -14,7 +14,7 @@ } -/** spinner.twig **/ +/** spinner.j2 **/ .sk-fading-circle { margin-top: 10px; diff --git a/localwebsite/htdocs/assets/app.js b/web/kbn_assets/app.js index 37f1307..d575a5a 100644 --- a/localwebsite/htdocs/assets/app.js +++ b/web/kbn_assets/app.js @@ -316,4 +316,53 @@ window.Cameras = { return video.canPlayType('application/vnd.apple.mpegurl'); }, }; -})();
\ No newline at end of file +})(); + + +class ModemStatusUpdater { + constructor(id) { + this.id = id; + this.elem = ge('modem_data_'+id); + this.fetch() + } + + fetch() { + ajax.get('/modems/info.ajx', { + id: this.id + }).then(({response}) => { + const {html} = response; + this.elem.innerHTML = html; + + // TODO enqueue rerender + }); + } +} + + +var ModemStatus = { + _modems: [], + + init: function(modems) { + for (var i = 0; i < modems.length; i++) { + var modem = modems[i]; + this._modems.push(new ModemStatusUpdater(modem)); + } + } +}; + + +var Inverter = { + poll: function () { + setInterval(this._tick, 1000); + }, + + _tick: function() { + ajax.get('/inverter.ajx') + .then(({response}) => { + if (response) { + var el = document.getElementById('inverter_status'); + el.innerHTML = response.html; + } + }); + } +};
\ No newline at end of file diff --git a/localwebsite/htdocs/assets/bootstrap.min.css b/web/kbn_assets/bootstrap.min.css index edfbbb0..edfbbb0 100644 --- a/localwebsite/htdocs/assets/bootstrap.min.css +++ b/web/kbn_assets/bootstrap.min.css diff --git a/localwebsite/htdocs/assets/bootstrap.min.js b/web/kbn_assets/bootstrap.min.js index aed031f..aed031f 100644 --- a/localwebsite/htdocs/assets/bootstrap.min.js +++ b/web/kbn_assets/bootstrap.min.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js index 9a9f036..9a9f036 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js +++ b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js index e877ade..e877ade 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js +++ b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js index fd26bc7..fd26bc7 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js +++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm Binary files differindex de5b4f7..de5b4f7 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js b/web/kbn_assets/h265webjs-dist/missile-120func.js index fd26bc7..fd26bc7 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js +++ b/web/kbn_assets/h265webjs-dist/missile-120func.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js index fb8f13d..fb8f13d 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js +++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm Binary files differindex ee7d92a..ee7d92a 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js b/web/kbn_assets/h265webjs-dist/missile-256mb.js index fb8f13d..fb8f13d 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js +++ b/web/kbn_assets/h265webjs-dist/missile-256mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js index 49ec3b6..49ec3b6 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js +++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm Binary files differindex 71432e4..71432e4 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js b/web/kbn_assets/h265webjs-dist/missile-512mb.js index 49ec3b6..49ec3b6 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js +++ b/web/kbn_assets/h265webjs-dist/missile-512mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js b/web/kbn_assets/h265webjs-dist/missile-format.js index 8f7eddf..8f7eddf 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js +++ b/web/kbn_assets/h265webjs-dist/missile-format.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-v20221120.js index c498b84..c498b84 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js +++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm Binary files differindex 629ce98..629ce98 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm +++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile.js b/web/kbn_assets/h265webjs-dist/missile.js index c498b84..c498b84 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/missile.js +++ b/web/kbn_assets/h265webjs-dist/missile.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js b/web/kbn_assets/h265webjs-dist/raw-parser.js index edc91a3..edc91a3 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js +++ b/web/kbn_assets/h265webjs-dist/raw-parser.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js index e845d0e..e845d0e 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js +++ b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js index 2e5d0ea..2e5d0ea 100644 --- a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js +++ b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js diff --git a/localwebsite/htdocs/assets/hls.js b/web/kbn_assets/hls.js index ce60c4f..ce60c4f 100644 --- a/localwebsite/htdocs/assets/hls.js +++ b/web/kbn_assets/hls.js diff --git a/localwebsite/htdocs/assets/polyfills.js b/web/kbn_assets/polyfills.js index e851999..e851999 100644 --- a/localwebsite/htdocs/assets/polyfills.js +++ b/web/kbn_assets/polyfills.js diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 new file mode 100644 index 0000000..e2e29e3 --- /dev/null +++ b/web/kbn_templates/base.j2 @@ -0,0 +1,44 @@ +{% macro breadcrumbs(history) %} + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="main.cgi">Главная</a></li> + {% for item in history %} + <li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}> + {% if item.link %}<a href="{{ item.link }}">{% endif %} + {% if item.html %} + {% raw %}{{ item.html }}{% endraw %} + {% else %} + {{ item.text }} + {% endif %} + {% if item.link %}</a>{% endif %} + </li> + {% endfor %} + </ol> + </nav> +{% endmacro %} + +<!doctype html> +<html> +<head> + <title>{{ title }}</title> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <script> + window.onerror = function(error) { + window.console && console.error(error); + } + </script> + {{ head_static | safe }} +</head> +<body> +<div class="container py-3"> + +{% block content %}{% endblock %} + +<script> +{% block js %}{% endblock %} +</script> + +</div> +</body> +</html> diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2 new file mode 100644 index 0000000..c356326 --- /dev/null +++ b/web/kbn_templates/index.j2 @@ -0,0 +1,39 @@ +{% extends "base.j2" %} + +{% block content %} +<div class="container py-4"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active" aria-current="page">Главная</li> + </ol> + </nav> + +<!-- {% if auth_user %}--> +<!-- <div class="mb-4 alert alert-secondary">--> +<!-- Вы авторизованы как <b>{{ auth_user.username }}</b>. <a href="/deauth/">Выйти</a>--> +<!-- </div>--> +<!-- {% endif %}--> + + <h6>Интернет</h6> + <ul class="list-group list-group-flush"> + <li class="list-group-item"><a href="/modems.cgi">Модемы</a></li> + <li class="list-group-item"><a href="/routing.cgi">Маршрутизация</a></li> + <li class="list-group-item"><a href="/sms.cgi">SMS-сообщения</a></li> + </ul> + + <h6 class="mt-4">Другое</h6> + <ul class="list-group list-group-flush"> + <li class="list-group-item"><a href="/inverter.cgi">Инвертор</a> (<a href="{{ inverter_grafana_url }}">Grafana</a>)</li> + <li class="list-group-item"><a href="/pump.cgi">Насос</a></li> + <li class="list-group-item"><a href="/sensors.cgi">Датчики</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li> + </ul> + + <h6 class="mt-4"><a href="/cams/"><b>Все камеры</b></a> (<a href="/cams/?high=1">HQ</a>)</h6> + <ul class="list-group list-group-flush"> + {% for id, name in cameras %} + <li class="list-group-item"><a href="/cams/{{ id }}/">{{ name }}</a> (<a href="/cams/{{ id }}/?high=1">HQ</a>)</li> + {% endfor %} + <li class="list-group-item"><a href="/cams/stat/">Статистика</a></li> + </ul> +</div> +{% endblock %}
\ No newline at end of file diff --git a/web/kbn_templates/inverter.j2 b/web/kbn_templates/inverter.j2 new file mode 100644 index 0000000..26491f3 --- /dev/null +++ b/web/kbn_templates/inverter.j2 @@ -0,0 +1,20 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'Инвертор'}]) }} + +<h6 class="text-primary">Статус</h6> +<div id="inverter_status"> + {{ html|safe }} +</div> + +<div class="pt-3"> + <a href="/inverter.cgi?do=set-osp&value={{ 'sub' if rated.output_source_priority == 'Solar-Battery-Utility' else 'sbu' }}"> + <button type="button" class="btn btn-primary">Переключить на <b>{{ 'Solar-Utility-Battery' if rated.output_source_priority == 'Solar-Battery-Utility' else 'Solar-Battery-Utility' }}</b></button> + </a> +</div> +{% endblock %} + +{% block js %} +Inverter.poll(); +{% endblock %}
\ No newline at end of file diff --git a/web/kbn_templates/loading.j2 b/web/kbn_templates/loading.j2 new file mode 100644 index 0000000..d064a48 --- /dev/null +++ b/web/kbn_templates/loading.j2 @@ -0,0 +1,14 @@ +<div class="sk-fading-circle"> + <div class="sk-circle1 sk-circle"></div> + <div class="sk-circle2 sk-circle"></div> + <div class="sk-circle3 sk-circle"></div> + <div class="sk-circle4 sk-circle"></div> + <div class="sk-circle5 sk-circle"></div> + <div class="sk-circle6 sk-circle"></div> + <div class="sk-circle7 sk-circle"></div> + <div class="sk-circle8 sk-circle"></div> + <div class="sk-circle9 sk-circle"></div> + <div class="sk-circle10 sk-circle"></div> + <div class="sk-circle11 sk-circle"></div> + <div class="sk-circle12 sk-circle"></div> +</div>
\ No newline at end of file diff --git a/web/kbn_templates/modem_data.j2 b/web/kbn_templates/modem_data.j2 new file mode 100644 index 0000000..7f97b77 --- /dev/null +++ b/web/kbn_templates/modem_data.j2 @@ -0,0 +1,13 @@ +{% with level=modem_data.level %} + <span class="text-secondary">Сигнал:</span> {% include 'signal_level.j2' %}<br> +{% endwith %} + +<span class="text-secondary">Тип сети:</span> <b>{{ modem_data.type }}</b><br> +<span class="text-secondary">RSSI:</span> {{ modem_data.rssi }}<br/> +{% if modem_data.sinr %} +<span class="text-secondary">SINR:</span> {{ modem_data.sinr }}<br/> +{% endif %} +<span class="text-secondary">Время соединения:</span> {{ modem_data.connected_time }}<br> +<span class="text-secondary">Принято/передано:</span> {{ modem_data.downloaded }} / {{ modem_data.uploaded }} +<br> +<a href="/modems/verbose.cgi?id={{ modem }}">Подробная информация</a> diff --git a/web/kbn_templates/modem_verbose.j2 b/web/kbn_templates/modem_verbose.j2 new file mode 100644 index 0000000..7c6c930 --- /dev/null +++ b/web/kbn_templates/modem_verbose.j2 @@ -0,0 +1,18 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([ + {'link': '/modems.cgi', 'text': "Модемы"}, + {'text': modem_name} +]) }} + +{% for item in data %} + {% set item_name = item[0] %} + {% set item_data = item[1] %} + <h6 class="text-primary mt-4">{{ item_name }}</h6> + {% for k, v in item_data.items() %} + {{ k }} = {{ v }}<br> + {% endfor %} +{% endfor %} + +{% endblock %}
\ No newline at end of file diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 new file mode 100644 index 0000000..06339f8 --- /dev/null +++ b/web/kbn_templates/modems.j2 @@ -0,0 +1,16 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'Модемы'}]) }} + +{% for modem in modems %} +<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ modems.getfullname(modem) }}</h6> +<div id="modem_data_{{ modem }}"> + {% include "loading.j2" %} +</div> +{% endfor %} +{% endblock %} + +{% block js %} +ModemStatus.init({{ modems.getkeys()|tojson }}); +{% endblock %} diff --git a/localwebsite/templates-web/pump.twig b/web/kbn_templates/pump.j2 index 3bce0e2..28d5c9d 100644 --- a/localwebsite/templates-web/pump.twig +++ b/web/kbn_templates/pump.j2 @@ -1,11 +1,10 @@ -{% include 'bc.twig' with { - history: [ - {text: "Насос" } - ] -} %} +{% extends "base.j2" %} -<form action="/pump/" method="get"> - <input type="hidden" name="set" value="{{ status == 'on' ? 'off' : 'on' }}" /> +{% block content %} +{{ breadcrumbs([{'text': 'Насос'}]) }} + +<form action="/pump.cgi" method="get"> + <input type="hidden" name="set" value="{{ 'off' if status == 'on' else 'on' }}" /> Сейчас насос {% if status == 'on' %} <span class="text-success"><b>включен</b></span>.<br><br> @@ -14,4 +13,5 @@ <span class="text-danger"><b>выключен</b></span>.<br><br> <button type="submit" class="btn btn-primary">Включить</button> {% endif %} -</form>
\ No newline at end of file +</form> +{% endblock %} diff --git a/localwebsite/templates-web/signal_level.twig b/web/kbn_templates/signal_level.j2 index 9498482..93c9abf 100644 --- a/localwebsite/templates-web/signal_level.twig +++ b/web/kbn_templates/signal_level.j2 @@ -1,5 +1,5 @@ <div class="signal_level"> - {% for i in 0..4 %} + {% for i in range(5) %} <div{% if i < level %} class="yes"{% endif %}></div> {% endfor %} </div>
\ No newline at end of file diff --git a/localwebsite/templates-web/sms_page.twig b/web/kbn_templates/sms.j2 index 112fa64..6de9d42 100644 --- a/localwebsite/templates-web/sms_page.twig +++ b/web/kbn_templates/sms.j2 @@ -1,14 +1,13 @@ -{% include 'bc.twig' with { - history: [ - {text: "SMS-сообщения" } - ] -} %} +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'SMS-сообщения'}]) }} <nav> <div class="nav nav-tabs" id="nav-tab"> - {% for modem in modems_list %} - {% if selected_modem != modem %}<a href="/sms/?modem={{ modem }}" class="text-decoration-none">{% endif %} - <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems[modem].short_label }}</button> + {% for modem in modems.keys() %} + {% if selected_modem != modem %}<a href="/sms.cgi?id={{ modem }}" class="text-decoration-none">{% endif %} + <button class="nav-link{% if modem == selected_modem %} active{% endif %}" type="button">{{ modems.getshortname(modem) }}</button> {% if selected_modem != modem %}</a>{% endif %} {% endfor %} </div> @@ -20,14 +19,14 @@ <div class="alert alert-success" role="alert"> Сообщение отправлено. </div> -{% elseif error %} +{% elif error %} <div class="alert alert-danger" role="alert"> {{ error }} </div> {% endif %} <div> - <form method="post" action="/sms/"> + <form method="post" action="/sms.cgi"> <input type="hidden" name="modem" value="{{ selected_modem }}"> <div class="form-floating mb-3"> <input type="text" name="phone" class="form-control" id="inputPhone" placeholder="+7911xxxyyzz"> @@ -46,17 +45,19 @@ <h6 class="text-primary mt-4"> Последние {% if not is_outbox %} - <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms/?modem={{ selected_modem }}&outbox=1">исходящие</a> + <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&outbox=1">исходящие</a> {% else %} - <a href="/sms/?modem={{ selected_modem }}">входящие</a> <span class="text-black-50">|</span> <b>исходящие</b> + <a href="/sms.cgi?id={{ selected_modem }}">входящие</a> <span class="text-black-50">|</span> <b>исходящие</b> {% endif %} </h6> {% for m in messages %} <div class="mt-3"> - <b>{{ m.phone }}</b> <span class="text-secondary">({{ m.date }})</span><br/> - {{ m.content }} + <b>{{ m.Phone }}</b> <span class="text-secondary">({{ m.Date }})</span><br/> + {{ m.Content }} </div> {% else %} <span class="text-secondary">Сообщений нет.</span> -{% endfor %}
\ No newline at end of file +{% endfor %} + +{% endblock %}
\ No newline at end of file |