aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-17 03:08:25 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-17 03:08:25 +0300
commit0ce2e41a2bad790c5232fafb4b6ed631ca8cd957 (patch)
treefd401495b87cae8c95a4c4edf2c851c8177b6069
parente9fc2c1835f7ac8e072919df81a6661c6308dea9 (diff)
parentb7f1d55c9b4de4d21b11e5615a5dc8be0d4e883c (diff)
merge with master
-rw-r--r--.gitignore13
-rw-r--r--README.md6
-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.py9
-rwxr-xr-xbin/camera_node.py (renamed from src/camera_node.py)11
-rwxr-xr-xbin/electricity_calc.py (renamed from src/electricity_calc.py)6
-rwxr-xr-xbin/esp32_capture.py (renamed from src/esp32_capture.py)7
-rwxr-xr-xbin/esp32cam_capture_diff_node.py (renamed from src/esp32cam_capture_diff_node.py)13
-rwxr-xr-xbin/gpiorelayd.py31
-rwxr-xr-xbin/inverter_bot.py (renamed from src/inverter_bot.py)269
-rwxr-xr-xbin/inverter_mqtt_util.py (renamed from src/inverter_mqtt_util.py)10
-rwxr-xr-xbin/inverterd_emulator.py (renamed from src/inverterd_emulator.py)3
-rwxr-xr-xbin/ipcam_capture.py142
-rwxr-xr-xbin/ipcam_motion_worker.sh (renamed from tools/ipcam_motion_worker.sh)2
-rwxr-xr-xbin/ipcam_ntp_util.py199
-rwxr-xr-xbin/ipcam_server.py (renamed from src/ipcam_server.py)225
-rwxr-xr-xbin/lugovaya_pump_mqtt_bot.py207
-rwxr-xr-xbin/mqtt_node_util.py (renamed from src/mqtt_node_util.py)64
-rwxr-xr-xbin/openwrt_log_analyzer.py79
-rwxr-xr-xbin/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-xbin/pio_ini.py (renamed from src/pio_ini.py)30
-rwxr-xr-xbin/polaris_kettle_bot.py (renamed from src/polaris_kettle_bot.py)13
-rwxr-xr-xbin/polaris_kettle_util.py (renamed from src/polaris_kettle_util.py)5
-rwxr-xr-xbin/pump_bot.py (renamed from src/pump_bot.py)169
-rwxr-xr-xbin/pump_mqtt_bot.py (renamed from src/pump_mqtt_bot.py)14
-rwxr-xr-xbin/relay_mqtt_bot.py (renamed from src/relay_mqtt_bot.py)76
-rwxr-xr-xbin/relay_mqtt_http_proxy.py134
-rwxr-xr-xbin/sensors_bot.py (renamed from src/sensors_bot.py)17
-rwxr-xr-xbin/sound_bot.py (renamed from src/sound_bot.py)31
-rwxr-xr-xbin/sound_node.py (renamed from src/sound_node.py)9
-rwxr-xr-xbin/sound_sensor_node.py (renamed from src/sound_sensor_node.py)9
-rwxr-xr-xbin/sound_sensor_server.py (renamed from src/sound_sensor_server.py)21
-rwxr-xr-xbin/ssh_tunnels_config_util.py (renamed from src/ssh_tunnels_config_util.py)6
-rwxr-xr-xbin/temphum_mqtt_node.py (renamed from src/temphum_mqtt_node.py)7
-rwxr-xr-xbin/temphum_mqtt_receiver.py (renamed from src/temphum_mqtt_receiver.py)8
-rwxr-xr-xbin/temphum_nodes_util.py (renamed from src/temphum_nodes_util.py)4
-rwxr-xr-xbin/temphum_smbus_util.py (renamed from src/temphum_smbus_util.py)6
-rwxr-xr-xbin/temphumd.py (renamed from src/temphumd.py)7
-rwxr-xr-xbin/web_api.py (renamed from src/web_api.py)38
-rw-r--r--bin/web_kbn.py354
-rw-r--r--doc/openwrt_logger.md28
-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.json12
-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.json11
-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.json11
-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-xinclude/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)bin7886 -> 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__.py19
-rw-r--r--include/py/homekit/api/__init__.pyi5
-rw-r--r--include/py/homekit/api/config.py15
-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__.py2
-rw-r--r--include/py/homekit/camera/config.py141
-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.py58
-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.py9
-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__.py2
-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.py13
-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__.py2
-rw-r--r--include/py/homekit/modem/config.py29
-rw-r--r--include/py/homekit/modem/e3372.py253
-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.php310
-rw-r--r--localwebsite/classes/GPIORelaydClient.php18
-rw-r--r--localwebsite/classes/InverterdClient.php69
-rw-r--r--localwebsite/classes/MyOpenWrtUtils.php10
-rw-r--r--localwebsite/handlers/InverterHandler.php102
-rw-r--r--localwebsite/handlers/MiscHandler.php41
-rw-r--r--localwebsite/handlers/ModemHandler.php197
-rw-r--r--localwebsite/htdocs/assets/inverter.js15
-rw-r--r--localwebsite/htdocs/assets/modem.js29
-rw-r--r--localwebsite/htdocs/index.php9
-rw-r--r--localwebsite/templates-web/index.twig6
-rw-r--r--localwebsite/templates-web/inverter_page.twig20
-rw-r--r--localwebsite/templates-web/modem_data.twig14
-rw-r--r--localwebsite/templates-web/modem_status_page.twig19
-rw-r--r--localwebsite/templates-web/modem_verbose_page.twig15
-rw-r--r--localwebsite/templates-web/routing_header.twig2
-rw-r--r--localwebsite/templates-web/spinner.twig14
-rw-r--r--misc/home_linux_boards/etc/default/homekit_ipcam_server2
-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-xmisc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh (renamed from tools/sunxi-setup-amixer.sh)0
-rwxr-xr-xmisc/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.json12
-rw-r--r--platformio/common/libs/mqtt_module_ota/library.json11
-rw-r--r--platformio/common/libs/mqtt_module_relay/library.json11
-rw-r--r--platformio/dumb_mqtt/.gitignore3
-rw-r--r--platformio/relayctl/.gitignore3
-rw-r--r--platformio/temphum/.gitignore3
-rw-r--r--requirements.txt22
-rw-r--r--requirements_kettle.txt3
-rwxr-xr-xsrc/gpiorelayd.py23
-rw-r--r--src/home/api/__init__.py11
-rw-r--r--src/home/api/__init__.pyi4
-rw-r--r--src/home/camera/__init__.py1
-rw-r--r--src/home/camera/types.py5
-rw-r--r--src/home/http/__init__.py2
-rw-r--r--src/home/inverter/config.py13
-rwxr-xr-xsrc/openwrt_log_analyzer.py72
-rwxr-xr-xsrc/relay_mqtt_http_proxy.py81
-rwxr-xr-xsrc/test_new_config.py11
-rw-r--r--systemd/camera_node.service2
-rw-r--r--systemd/camera_node@.service2
-rw-r--r--systemd/esp32cam_capture_diff_node.service2
-rw-r--r--systemd/gpiorelayd@.service5
-rw-r--r--systemd/inverter_bot.service2
-rw-r--r--systemd/inverter_mqtt_receiver.service4
-rw-r--r--systemd/inverter_mqtt_sender.service4
-rw-r--r--systemd/ipcam_capture@.service15
-rw-r--r--systemd/ipcam_rtsp2hls@.service16
-rw-r--r--systemd/ipcam_server.service5
-rw-r--r--systemd/polaris_kettle_bot.service2
-rw-r--r--systemd/pump_bot.service2
-rw-r--r--systemd/pump_mqtt_bot.service2
-rw-r--r--systemd/relay_mqtt_bot.service2
-rw-r--r--systemd/relay_mqtt_http_proxy.service2
-rw-r--r--systemd/sensors_bot.service2
-rw-r--r--systemd/sound_bot.service2
-rw-r--r--systemd/sound_node.service2
-rw-r--r--systemd/sound_sensor_node.service2
-rw-r--r--systemd/sound_sensor_server.service2
-rw-r--r--systemd/temphumd.service2
-rw-r--r--systemd/temphumd@.service2
-rw-r--r--tasks/df_h.sh2
-rw-r--r--test/__init__.py0
-rw-r--r--test/__py_include.py9
-rwxr-xr-xtest/mqtt_relay_server_util.py19
-rwxr-xr-xtest/mqtt_relay_util.py15
-rwxr-xr-xtest/test.py7
-rwxr-xr-xtest/test_amixer.py9
-rwxr-xr-xtest/test_api.py17
-rwxr-xr-xtest/test_esp32_cam.py16
-rwxr-xr-xtest/test_inverter_monitor.py19
-rw-r--r--test/test_ipcam_server_cleanup.py14
-rwxr-xr-xtest/test_modems.py9
-rwxr-xr-xtest/test_polaris_stuff.py11
-rwxr-xr-xtest/test_record_upload.py21
-rwxr-xr-xtest/test_send_fake_sound_hit.py12
-rwxr-xr-xtest/test_sensors_plot.py0
-rwxr-xr-xtest/test_sound_node_client.py9
-rwxr-xr-xtest/test_sound_server_api.py16
-rwxr-xr-xtest/test_stopwatch.py3
-rw-r--r--test/test_telegram_aio_send_photo.py13
-rwxr-xr-xtools/ipcam_capture.sh119
-rwxr-xr-xtools/ipcam_rtsp2hls.sh127
-rwxr-xr-xtools/process-motion-timecodes.py61
-rwxr-xr-xtools/rotate-video.sh2
-rwxr-xr-xtools/video-util.sh2
-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)bin2190151 -> 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)bin2108889 -> 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)bin2108889 -> 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)bin2108891 -> 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.j244
-rw-r--r--web/kbn_templates/index.j239
-rw-r--r--web/kbn_templates/inverter.j220
-rw-r--r--web/kbn_templates/loading.j214
-rw-r--r--web/kbn_templates/modem_data.j213
-rw-r--r--web/kbn_templates/modem_verbose.j218
-rw-r--r--web/kbn_templates/modems.j216
-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
diff --git a/.gitignore b/.gitignore
index 4ffc1b1..b113ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index 5897142..7979cf7 100644
--- a/README.md
+++ b/README.md
@@ -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
index 6940e4f..6940e4f 100644
--- a/platformio/common/static/favicon.ico
+++ b/include/pio/static/favicon.ico
Binary files differ
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
index de5b4f7..de5b4f7 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm
Binary files differ
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
index ee7d92a..ee7d92a 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm
Binary files differ
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
index 71432e4..71432e4 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm
Binary files differ
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
index 629ce98..629ce98 100644
--- a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm
+++ b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm
Binary files differ
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&amp;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 }}&amp;outbox=1">исходящие</a>
+ <b>входящие</b> <span class="text-black-50">|</span> <a href="/sms.cgi?id={{ selected_modem }}&amp;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