aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-09-27 00:54:57 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-09-27 00:54:57 +0300
commitd3a295872c49defb55fc8e4e43e55550991e0927 (patch)
treeb9dca15454f9027d5a9dad0d4443a20de04dbc5d
parentb7cbc2571c1870b4582ead45277d0aa7f961bec8 (diff)
parentbdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 (diff)
Merge branch 'master' of ch1p.io:homekit
-rw-r--r--.gitignore12
-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)13
-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)15
-rwxr-xr-xbin/gpiorelayd.py31
-rwxr-xr-xbin/inverter_bot.py (renamed from src/inverter_bot.py)355
-rwxr-xr-xbin/inverter_mqtt_util.py27
-rwxr-xr-xbin/inverterd_emulator.py (renamed from src/inverterd_emulator.py)3
-rwxr-xr-xbin/ipcam_capture.py141
-rwxr-xr-xbin/ipcam_motion_worker.sh (renamed from tools/ipcam_motion_worker.sh)2
-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.py68
-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)38
-rwxr-xr-xbin/polaris_kettle_bot.py (renamed from src/polaris_kettle_bot.py)19
-rwxr-xr-xbin/polaris_kettle_util.py (renamed from src/polaris_kettle_util.py)9
-rwxr-xr-xbin/pump_bot.py297
-rwxr-xr-xbin/pump_mqtt_bot.py (renamed from src/pump_mqtt_bot.py)33
-rwxr-xr-xbin/relay_mqtt_bot.py164
-rwxr-xr-xbin/relay_mqtt_http_proxy.py134
-rwxr-xr-xbin/sensors_bot.py (renamed from src/sensors_bot.py)19
-rwxr-xr-xbin/sound_bot.py (renamed from src/sound_bot.py)33
-rwxr-xr-xbin/sound_node.py (renamed from src/sound_node.py)11
-rwxr-xr-xbin/sound_sensor_node.py (renamed from src/sound_sensor_node.py)11
-rwxr-xr-xbin/sound_sensor_server.py (renamed from src/sound_sensor_server.py)23
-rwxr-xr-xbin/ssh_tunnels_config_util.py (renamed from src/ssh_tunnels_config_util.py)8
-rwxr-xr-xbin/temphum_mqtt_node.py79
-rwxr-xr-xbin/temphum_mqtt_receiver.py (renamed from src/sensors_mqtt_receiver.py)27
-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)5
-rwxr-xr-xbin/temphumd.py (renamed from src/temphumd.py)10
-rwxr-xr-xbin/web_api.py (renamed from src/web_api.py)40
-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)21
-rw-r--r--include/pio/libs/main/homekit/main.h (renamed from platformio/common/libs/main/homekit/main.h)4
-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)2
-rw-r--r--include/pio/libs/mqtt/homekit/mqtt/module.h (renamed from platformio/common/libs/mqtt/homekit/mqtt/module.h)15
-rw-r--r--include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp (renamed from platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp)29
-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)11
-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)5
-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)12
-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)7
-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)27
-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)10
-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)2
-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)2
-rw-r--r--include/pio/libs/mqtt_module_temphum/library.json11
-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)0
-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.py130
-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.py46
-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__.py14
-rw-r--r--include/py/homekit/config/_configs.py61
-rw-r--r--include/py/homekit/config/config.py406
-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)2
-rw-r--r--include/py/homekit/database/inverter.py (renamed from src/home/database/inverter.py)0
-rw-r--r--include/py/homekit/database/inverter_time_formats.py (renamed from src/home/database/inverter_time_formats.py)0
-rw-r--r--include/py/homekit/database/mysql.py (renamed from src/home/database/mysql.py)0
-rw-r--r--include/py/homekit/database/sensors.py (renamed from src/home/database/sensors.py)0
-rw-r--r--include/py/homekit/database/simple_state.py (renamed from src/home/database/simple_state.py)14
-rw-r--r--include/py/homekit/database/sqlite.py (renamed from src/home/database/sqlite.py)22
-rw-r--r--include/py/homekit/http/__init__.py (renamed from src/home/http/__init__.py)0
-rw-r--r--include/py/homekit/http/http.py (renamed from src/home/http/http.py)0
-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)1
-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/mqtt/__init__.py7
-rw-r--r--include/py/homekit/mqtt/_config.py165
-rw-r--r--include/py/homekit/mqtt/_module.py70
-rw-r--r--include/py/homekit/mqtt/_mqtt.py (renamed from src/home/mqtt/mqtt.py)55
-rw-r--r--include/py/homekit/mqtt/_node.py92
-rw-r--r--include/py/homekit/mqtt/_payload.py (renamed from src/home/mqtt/payload/base_payload.py)4
-rw-r--r--include/py/homekit/mqtt/_util.py15
-rw-r--r--include/py/homekit/mqtt/_wrapper.py60
-rw-r--r--include/py/homekit/mqtt/module/diagnostics.py (renamed from src/home/mqtt/payload/esp.py)56
-rw-r--r--include/py/homekit/mqtt/module/inverter.py195
-rw-r--r--include/py/homekit/mqtt/module/ota.py77
-rw-r--r--include/py/homekit/mqtt/module/relay.py92
-rw-r--r--include/py/homekit/mqtt/module/temphum.py82
-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)14
-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.py78
-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__.py1
-rw-r--r--include/py/homekit/temphum/base.py (renamed from src/home/temphum/base.py)20
-rw-r--r--include/py/homekit/temphum/i2c.py52
-rw-r--r--include/py/homekit/util.py (renamed from src/home/util.py)97
-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--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
-rw-r--r--misc/openwrt/etc/rc.local2
-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)1
-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/common/libs/mqtt_module_temphum/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.txt21
-rw-r--r--requirements_kettle.txt3
-rwxr-xr-xsrc/esp_mqtt_util.py42
-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/config/__init__.py1
-rw-r--r--src/home/config/config.py204
-rw-r--r--src/home/mqtt/__init__.py4
-rw-r--r--src/home/mqtt/esp.py106
-rw-r--r--src/home/mqtt/payload/__init__.py1
-rw-r--r--src/home/mqtt/payload/inverter.py73
-rw-r--r--src/home/mqtt/payload/relay.py22
-rw-r--r--src/home/mqtt/payload/sensors.py20
-rw-r--r--src/home/mqtt/payload/temphum.py15
-rw-r--r--src/home/mqtt/relay.py71
-rw-r--r--src/home/mqtt/temphum.py54
-rw-r--r--src/home/mqtt/util.py8
-rw-r--r--src/home/temphum/__init__.py18
-rw-r--r--src/home/temphum/dht12.py22
-rw-r--r--src/home/temphum/si7021.py13
-rwxr-xr-xsrc/inverter_mqtt_receiver.py74
-rwxr-xr-xsrc/inverter_mqtt_sender.py72
-rwxr-xr-xsrc/openwrt_log_analyzer.py72
-rwxr-xr-xsrc/pump_bot.py131
-rwxr-xr-xsrc/relay_mqtt_bot.py112
-rwxr-xr-xsrc/relay_mqtt_http_proxy.py67
-rwxr-xr-xsrc/sensors_mqtt_sender.py58
-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.service13
-rw-r--r--systemd/inverter_mqtt_sender.service2
-rw-r--r--systemd/ipcam_capture@.service15
-rw-r--r--systemd/ipcam_rtsp2hls@.service14
-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/sensors_mqtt_receiver.service4
-rw-r--r--systemd/sensors_mqtt_sender.service13
-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--test/__init__.py0
-rw-r--r--test/__py_include.py9
-rwxr-xr-xtest/mqtt_relay_server_util.py11
-rwxr-xr-xtest/mqtt_relay_util.py32
-rwxr-xr-xtest/test.py7
-rwxr-xr-xtest/test_amixer.py11
-rwxr-xr-xtest/test_api.py19
-rwxr-xr-xtest/test_esp32_cam.py18
-rwxr-xr-xtest/test_inverter_monitor.py21
-rw-r--r--test/test_ipcam_server_cleanup.py16
-rwxr-xr-xtest/test_polaris_stuff.py11
-rwxr-xr-xtest/test_record_upload.py23
-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.py18
-rwxr-xr-xtest/test_stopwatch.py3
-rw-r--r--test/test_telegram_aio_send_photo.py15
-rwxr-xr-xtools/ipcam_capture.sh119
-rwxr-xr-xtools/ipcam_rtsp2hls.sh127
-rwxr-xr-xtools/mcuota.py98
-rwxr-xr-xtools/mcuota.sh14
-rwxr-xr-xtools/process-motion-timecodes.py61
-rwxr-xr-xtools/rotate-video.sh2
-rwxr-xr-xtools/video-util.sh2
292 files changed, 4180 insertions, 2852 deletions
diff --git a/.gitignore b/.gitignore
index 5f65bca..b113ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,17 +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
+/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 d175e17..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.
@@ -65,7 +66,7 @@ class ESP32CameraNodeServer(MediaNodeServer):
if __name__ == '__main__':
- config.load('camera_node')
+ config.load_app('camera_node')
recorder_kwargs = {}
camera_type = CameraType(config['camera']['type'])
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 4363e9e..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'])
@@ -76,7 +77,7 @@ class ESP32CamCaptureDiffNode:
if __name__ == '__main__':
- config.load('esp32cam_capture_diff_node')
+ config.load_app('esp32cam_capture_diff_node')
loop = asyncio.get_event_loop()
ESP32CamCaptureDiffNode()
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 fd5acf3..032f513 100755
--- a/src/inverter_bot.py
+++ b/bin/inverter_bot.py
@@ -4,32 +4,40 @@ import re
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
-from home.telegram import bot
-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 *
-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
-monitor: Optional[InverterMonitor] = None
+
+if __name__ != '__main__':
+ print(f'this script can not be imported as module', file=sys.stderr)
+ sys.exit(1)
+
+
db = None
LT = escape('<=')
flags_map = {
@@ -42,9 +50,56 @@ flags_map = {
'alarm_on_on_primary_source_interrupt': 'ALRM',
'fault_code_record': 'FTCR',
}
-
logger = logging.getLogger(__name__)
-config.load('inverter_bot')
+
+
+class InverterBotConfig(AppConfigUnit, TelegramBotConfig):
+ NAME = 'inverter_bot'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ acmode_item_schema = {
+ 'thresholds': {
+ 'type': 'list',
+ 'required': True,
+ 'schema': {
+ 'type': 'list',
+ 'min': 40,
+ 'max': 60
+ },
+ },
+ 'initial_current': {'type': 'integer'}
+ }
+
+ return {
+ **super(TelegramBotConfig).schema(),
+ 'ac_mode': {
+ 'type': 'dict',
+ 'required': True,
+ 'schema': {
+ 'generator': acmode_item_schema,
+ 'utilities': acmode_item_schema
+ }
+ },
+ 'monitor': {
+ 'type': 'dict',
+ 'required': True,
+ 'schema': {
+ 'vlow': {'type': 'integer', 'required': True},
+ 'vcrit': {'type': 'integer', 'required': True},
+ 'gen_currents': {'type': 'list', 'schema': {'type': 'integer'}, 'required': True},
+ 'gen_raise_intervals': {'type': 'list', 'schema': {'type': 'integer'}, 'required': True},
+ 'gen_cur30_v_limit': {'type': 'float', 'required': True},
+ 'gen_cur20_v_limit': {'type': 'float', 'required': True},
+ 'gen_cur10_v_limit': {'type': 'float', 'required': True},
+ 'gen_floating_v': {'type': 'integer', 'required': True},
+ 'gen_floating_time_max': {'type': 'integer', 'required': True}
+ }
+ }
+ }
+
+
+config.load_app(InverterBotConfig)
bot.initialize()
bot.lang.ru(
@@ -293,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)
+ )
)
@@ -309,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)
+ )
)
@@ -321,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)
+ )
)
@@ -338,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]:
@@ -423,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,
@@ -436,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()}'):
@@ -458,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')
@@ -493,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')
@@ -606,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
@@ -661,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_'):
@@ -708,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']
@@ -719,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']
@@ -788,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)
@@ -822,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
@@ -863,28 +936,26 @@ class InverterStore(bot.BotDatabase):
self.commit()
-if __name__ == '__main__':
- inverter.init(host=config['inverter']['ip'], port=config['inverter']['port'])
+inverter.init(host=config['inverter']['ip'], port=config['inverter']['port'])
- bot.set_database(InverterStore())
- bot.enable_logging(BotType.INVERTER)
+bot.set_database(InverterStore())
- bot.add_conversation(SettingsConversation(enable_back=True))
- bot.add_conversation(ConsumptionConversation(enable_back=True))
+bot.add_conversation(SettingsConversation(enable_back=True))
+bot.add_conversation(ConsumptionConversation(enable_back=True))
- monitor = InverterMonitor()
- monitor.set_charging_event_handler(monitor_charging)
- monitor.set_battery_event_handler(monitor_battery)
- monitor.set_util_event_handler(monitor_util)
- monitor.set_error_handler(monitor_error)
- monitor.set_osp_need_change_callback(osp_change_cb)
+monitor = InverterMonitor()
+monitor.set_charging_event_handler(monitor_charging)
+monitor.set_battery_event_handler(monitor_battery)
+monitor.set_util_event_handler(monitor_util)
+monitor.set_error_handler(monitor_error)
+monitor.set_osp_need_change_callback(osp_change_cb)
- setacmode(getacmode())
+setacmode(getacmode())
- if not config.get('monitor.disabled'):
- logging.info('starting monitor')
- monitor.start()
+if not config.get('monitor.disabled'):
+ logging.info('starting monitor')
+ monitor.start()
- bot.run()
+bot.run()
- monitor.stop()
+monitor.stop()
diff --git a/bin/inverter_mqtt_util.py b/bin/inverter_mqtt_util.py
new file mode 100755
index 0000000..6003c62
--- /dev/null
+++ b/bin/inverter_mqtt_util.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+import __py_include
+
+from argparse import ArgumentParser
+from homekit.config import config
+from homekit.mqtt import MqttWrapper, MqttNode
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument('mode', type=str, choices=('sender', 'receiver'), nargs=1)
+
+ config.load_app('inverter_mqtt_util', parser=parser)
+ arg = parser.parse_args()
+ mode = arg.mode[0]
+
+ mqtt = MqttWrapper(client_id=f'inverter_mqtt_{mode}',
+ clean_session=mode != 'receiver')
+ node = MqttNode(node_id='inverter')
+ module_kwargs = {}
+ if mode == 'sender':
+ 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)
+
+ mqtt.connect_and_loop()
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..5de14af
--- /dev/null
+++ b/bin/ipcam_capture.py
@@ -0,0 +1,141 @@
+#!/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'
+ 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/src/ipcam_server.py b/bin/ipcam_server.py
index 2c4915d..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('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/bin/mqtt_node_util.py b/bin/mqtt_node_util.py
new file mode 100755
index 0000000..cf451fd
--- /dev/null
+++ b/bin/mqtt_node_util.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+import os.path
+import __py_include
+
+from time import sleep
+from typing import Optional
+from argparse import ArgumentParser, ArgumentError
+
+from homekit.config import config
+from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules
+from homekit.mqtt import MqttNodesConfig
+
+mqtt_node: Optional[MqttNode] = None
+mqtt: Optional[MqttWrapper] = None
+
+
+if __name__ == '__main__':
+ nodes_config = MqttNodesConfig()
+
+ parser = ArgumentParser()
+ parser.add_argument('--node-id', type=str, required=True, choices=nodes_config.get_nodes(only_names=True))
+ parser.add_argument('--modules', type=str, choices=get_mqtt_modules(), nargs='*',
+ help='mqtt modules to include')
+ parser.add_argument('--switch-relay', choices=[0, 1], type=int,
+ help='send relay state')
+ parser.add_argument('--legacy-relay', action='store_true')
+ parser.add_argument('--push-ota', type=str, metavar='OTA_FILENAME',
+ help='push OTA, receives path to firmware.bin')
+
+ config.load_app(parser=parser, no_config=True)
+ arg = parser.parse_args()
+
+ 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_node = MqttNode(node_id=arg.node_id,
+ node_secret=nodes_config.get_node(arg.node_id)['password'])
+
+ mqtt.add_node(mqtt_node)
+
+ # must-have modules
+ ota_module = mqtt_node.load_module('ota')
+ mqtt_node.load_module('diagnostics')
+
+ if arg.modules:
+ for m in arg.modules:
+ kwargs = {}
+ if m == 'relay' and arg.legacy_relay:
+ kwargs['legacy_topics'] = 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)
+
+ 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:
+ sleep(0.1)
+
+ except KeyboardInterrupt:
+ 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 3b19de2..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('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 19dd707..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)
@@ -54,12 +56,17 @@ def bsd_parser(product_config: dict,
arg_kwargs['type'] = int
elif kwargs['type'] == 'int':
arg_kwargs['type'] = int
+ elif kwargs['type'] == 'bool':
+ arg_kwargs['action'] = 'store_true'
+ arg_kwargs['required'] = False
else:
raise TypeError(f'unsupported type {kwargs["type"]} for define {define_name}')
else:
arg_kwargs['action'] = 'store_true'
- parser.add_argument(f'--{define_name}', required=True, **arg_kwargs)
+ if 'required' not in arg_kwargs:
+ arg_kwargs['required'] = True
+ parser.add_argument(f'--{define_name}', **arg_kwargs)
bsd_walk(product_config, f)
@@ -76,6 +83,10 @@ def bsd_get(product_config: dict,
enums.append(f'CONFIG_{define_name}')
defines[f'CONFIG_{define_name}'] = f'HOMEKIT_{attr_value.upper()}'
return
+ if kwargs['type'] == 'bool':
+ 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)
return defines, enums
@@ -98,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')
@@ -115,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 088707d..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 MqttBase
-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,
@@ -41,7 +41,7 @@ from telegram.ext import (
)
logger = logging.getLogger(__name__)
-config.load('polaris_kettle_bot')
+config.load_app('polaris_kettle_bot')
primary_choices = (70, 80, 90, 100)
all_choices = range(
@@ -204,7 +204,7 @@ class KettleInfo:
class KettleController(threading.Thread,
- MqttBase,
+ Mqtt,
DeviceListener,
IncomingMessageListener,
KettleInfoListener,
@@ -224,7 +224,7 @@ class KettleController(threading.Thread,
def __init__(self):
# basic setup
- MqttBase.__init__(self, clean_session=False)
+ Mqtt.__init__(self, clean_session=False)
threading.Thread.__init__(self)
self._logger = logging.getLogger(self.__class__.__name__)
@@ -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 81326dd..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 MqttBase
-from home.config import config
+from homekit.mqtt import Mqtt
+from homekit.config import config
from syncleo import (
Kettle,
PowerType,
@@ -21,7 +22,7 @@ logger = logging.getLogger(__name__)
control_tasks = SimpleQueue()
-class MqttServer(MqttBase):
+class MqttServer(Mqtt):
def __init__(self):
super().__init__(clean_session=False)
@@ -75,7 +76,7 @@ def main():
parser.add_argument('-t', '--temperature', dest='temp', type=int, default=tempmax,
choices=range(tempmin, tempmax+tempstep, tempstep))
- arg = config.load('polaris_kettle_util', use_cli=True, parser=parser)
+ arg = config.load_app('polaris_kettle_util', use_cli=True, parser=parser)
if arg.mode == 'mqtt':
server = MqttServer()
diff --git a/bin/pump_bot.py b/bin/pump_bot.py
new file mode 100755
index 0000000..e00e844
--- /dev/null
+++ b/bin/pump_bot.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+import __py_include
+import sys
+import asyncio
+
+from enum import Enum
+from typing import Optional, Union
+from telegram import ReplyKeyboardMarkup, User
+from time import time
+from datetime import datetime
+
+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'
+
+ @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]
+
+time_format = '%d.%m.%Y, %H:%M:%S'
+
+watering_mcu_status = {
+ 'last_time': 0,
+ 'last_boot_time': 0,
+ 'relay_opened': False,
+ 'ambient_temp': 0.0,
+ 'ambient_rh': 0.0,
+}
+
+bot.initialize()
+bot.lang.ru(
+ start_message="Выберите команду на клавиатуре",
+ unknown_command="Неизвестная команда",
+
+ enable="Включить",
+ enable_silently="Включить тихо",
+ enabled="Насос включен ✅",
+
+ disable="Выключить",
+ disable_silently="Выключить тихо",
+ disabled="Насос выключен ❌",
+
+ start_watering="Включить полив",
+ stop_watering="Отключить полив",
+
+ status="Статус насоса",
+ watering_status="Статус полива",
+
+ done="Готово 👌",
+ sent="Команда отправлена",
+
+ user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.',
+ user_watering_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> полив.',
+ user_action_on="включил",
+ user_action_off="выключил",
+ user_action_watering_on="включил",
+ user_action_watering_off="выключил",
+)
+bot.lang.en(
+ start_message="Select command on the keyboard",
+ unknown_command="Unknown command",
+
+ enable="Turn ON",
+ enable_silently="Turn ON silently",
+ enabled="The pump is turned ON ✅",
+
+ disable="Turn OFF",
+ disable_silently="Turn OFF silently",
+ disabled="The pump is turned OFF ❌",
+
+ start_watering="Start watering",
+ stop_watering="Stop watering",
+
+ status="Pump status",
+ watering_status="Watering status",
+
+ done="Done 👌",
+ sent="Request sent",
+
+ user_action_notification='User <a href="tg://user?id=%d">%s</a> turned the pump <b>%s</b>.',
+ user_watering_notification='User <a href="tg://user?id=%d">%s</a> <b>%s</b> the watering.',
+ user_action_on="ON",
+ user_action_off="OFF",
+ user_action_watering_on="started",
+ user_action_watering_off="stopped",
+)
+
+
+class UserAction(Enum):
+ ON = 'on'
+ OFF = 'off'
+ WATERING_ON = 'watering_on'
+ WATERING_OFF = 'watering_off'
+
+
+def get_relay() -> RelayClient:
+ relay = RelayClient(host=config.app_config['pump_relay_addr'].host,
+ port=config.app_config['pump_relay_addr'].port)
+ relay.connect()
+ return relay
+
+
+async def on(ctx: bot.Context, silent=False) -> None:
+ get_relay().on()
+ futures = [ctx.reply(ctx.lang('done'))]
+ if not silent:
+ futures.append(notify(ctx.user, UserAction.ON))
+ await asyncio.gather(*futures)
+
+
+async def off(ctx: bot.Context, silent=False) -> None:
+ get_relay().off()
+ futures = [ctx.reply(ctx.lang('done'))]
+ if not silent:
+ futures.append(notify(ctx.user, UserAction.OFF))
+ await asyncio.gather(*futures)
+
+
+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)
+ )
+
+
+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)
+ )
+
+
+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)
+
+ await bot.notify_all(text_getter, exclude=(user.id,))
+
+
+@bot.handler(message='enable')
+async def enable_handler(ctx: bot.Context) -> None:
+ await on(ctx)
+
+
+@bot.handler(message='enable_silently')
+async def enable_s_handler(ctx: bot.Context) -> None:
+ await on(ctx, True)
+
+
+@bot.handler(message='disable')
+async def disable_handler(ctx: bot.Context) -> None:
+ await off(ctx)
+
+
+@bot.handler(message='start_watering')
+async def start_watering(ctx: bot.Context) -> None:
+ await watering_on(ctx)
+
+
+@bot.handler(message='stop_watering')
+async def stop_watering(ctx: bot.Context) -> None:
+ await watering_off(ctx)
+
+
+@bot.handler(message='disable_silently')
+async def disable_s_handler(ctx: bot.Context) -> None:
+ await off(ctx, True)
+
+
+@bot.handler(message='status')
+async def status(ctx: bot.Context) -> None:
+ await ctx.reply(
+ ctx.lang('enabled') if get_relay().status() == 'on' else ctx.lang('disabled')
+ )
+
+
+def _get_timestamp_as_string(timestamp: int) -> str:
+ if timestamp != 0:
+ return datetime.fromtimestamp(timestamp).strftime(time_format)
+ else:
+ return 'unknown'
+
+
+@bot.handler(message='watering_status')
+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'
+ buf += f'last report time: <b>{_get_timestamp_as_string(watering_mcu_status["last_time"])}</b>\n'
+ if watering_mcu_status["last_boot_time"] != 0:
+ 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>'
+ await ctx.reply(buf)
+
+
+@bot.defaultreplymarkup
+def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
+ buttons = []
+ 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')])
+
+ return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
+
+
+def mqtt_payload_callback(mqtt_node: MqttNode, payload: MqttPayload):
+ global watering_mcu_status
+
+ types_the_node_can_send = (
+ InitialDiagnosticsPayload,
+ DiagnosticsPayload,
+ MqttTemphumDataPayload,
+ MqttPowerStatusPayload
+ )
+ for cl in types_the_node_can_send:
+ if isinstance(payload, cl):
+ watering_mcu_status['last_time'] = int(time())
+ break
+
+ if isinstance(payload, InitialDiagnosticsPayload):
+ watering_mcu_status['last_boot_time'] = int(time())
+
+ elif isinstance(payload, MqttTemphumDataPayload):
+ watering_mcu_status['ambient_temp'] = payload.temp
+ watering_mcu_status['ambient_rh'] = payload.rh
+
+ elif isinstance(payload, MqttPowerStatusPayload):
+ watering_mcu_status['relay_opened'] = payload.opened
+
+
+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.add_payload_callback(mqtt_payload_callback)
+
+mqtt.connect_and_loop(loop_forever=False)
+
+bot.run()
+
+try:
+ mqtt.disconnect()
+except:
+ pass
diff --git a/src/pump_mqtt_bot.py b/bin/pump_mqtt_bot.py
index d3b6de4..aea1451 100755
--- a/src/pump_mqtt_bot.py
+++ b/bin/pump_mqtt_bot.py
@@ -1,20 +1,20 @@
#!/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.esp import MqttEspDevice
-from home.mqtt import MqttRelay, MqttRelayState
-from home.mqtt.payload import MqttPayload
-from home.mqtt.payload.relay 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('pump_mqtt_bot')
+config.load_app('pump_mqtt_bot')
bot.initialize()
bot.lang.ru(
@@ -70,7 +70,7 @@ bot.lang.en(
)
-mqtt_relay: Optional[MqttRelay] = None
+mqtt: Optional[MqttNode] = None
relay_state = MqttRelayState()
@@ -99,14 +99,14 @@ def notify(user: User, action: UserAction) -> None:
@bot.handler(message='enable')
def enable_handler(ctx: bot.Context) -> None:
- mqtt_relay.set_power(config['mqtt']['home_id'], True)
+ mqtt.set_power(config['mqtt']['home_id'], True)
ctx.reply(ctx.lang('done'))
notify(ctx.user, UserAction.ON)
@bot.handler(message='disable')
def disable_handler(ctx: bot.Context) -> None:
- mqtt_relay.set_power(config['mqtt']['home_id'], False)
+ mqtt.set_power(config['mqtt']['home_id'], False)
ctx.reply(ctx.lang('done'))
notify(ctx.user, UserAction.OFF)
@@ -157,13 +157,12 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
if __name__ == '__main__':
- mqtt_relay = MqttRelay(devices=MqttEspDevice(id=config['mqtt']['home_id'],
- secret=config['mqtt']['home_secret']))
- mqtt_relay.set_message_callback(on_mqtt_message)
- mqtt_relay.configure_tls()
- mqtt_relay.connect_and_loop(loop_forever=False)
+ mqtt = MqttRelay(devices=MqttEspDevice(id=config['mqtt']['home_id'],
+ secret=config['mqtt']['home_secret']))
+ mqtt.set_message_callback(on_mqtt_message)
+ mqtt.connect_and_loop(loop_forever=False)
# bot.enable_logging(BotType.PUMP_MQTT)
bot.run(start_handler=start)
- mqtt_relay.disconnect()
+ mqtt.disconnect()
diff --git a/bin/relay_mqtt_bot.py b/bin/relay_mqtt_bot.py
new file mode 100755
index 0000000..3ad0a9b
--- /dev/null
+++ b/bin/relay_mqtt_bot.py
@@ -0,0 +1,164 @@
+#!/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 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__':
+ print(f'this script can not be imported as module', file=sys.stderr)
+ sys.exit(1)
+
+
+mqtt_nodes_config = MqttNodesConfig()
+
+
+class RelayMqttBotConfig(AppConfigUnit, TelegramBotConfig):
+ NAME = 'relay_mqtt_bot'
+
+ _strings: Translation
+
+ def __init__(self):
+ super().__init__()
+ self._strings = Translation('mqtt_nodes')
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ **super(TelegramBotConfig).schema(),
+ 'relay_nodes': {
+ 'type': 'list',
+ 'required': True,
+ 'schema': {
+ 'type': 'string'
+ }
+ },
+ }
+
+ @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 get_relay_name_translated(self, lang: str, relay_name: str) -> str:
+ return self._strings.get(lang)[relay_name]['relay']
+
+
+config.load_app(RelayMqttBotConfig)
+
+bot.initialize()
+bot.lang.ru(
+ start_message="Выберите команду на клавиатуре",
+ unknown_command="Неизвестная команда",
+ done="Готово 👌",
+)
+bot.lang.en(
+ start_message="Select command on the keyboard",
+ unknown_command="Unknown command",
+ done="Done 👌",
+)
+
+
+type_emojis = {
+ 'lamp': '💡'
+}
+status_emoji = {
+ 'on': '✅',
+ 'off': '❌'
+}
+
+
+mqtt: MqttWrapper
+relay_nodes: dict[str, Union[MqttRelayModule, MqttModule]] = {}
+relay_states: dict[str, MqttRelayState] = {}
+
+
+class UserAction(Enum):
+ ON = 'on'
+ OFF = 'off'
+
+
+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 isinstance(message, InitialDiagnosticsPayload):
+ kwargs['fw_version'] = message.fw_version
+ if node.id not in relay_states:
+ relay_states[node.id] = MqttRelayState()
+ relay_states[node.id].update(**kwargs)
+
+
+async def enable_handler(node_id: str, ctx: bot.Context) -> None:
+ relay_nodes[node_id].switchpower(True)
+ await ctx.reply(ctx.lang('done'))
+
+
+async def disable_handler(node_id: str, ctx: bot.Context) -> None:
+ relay_nodes[node_id].switchpower(False)
+ await ctx.reply(ctx.lang('done'))
+
+
+async def start(ctx: bot.Context) -> None:
+ await ctx.reply(ctx.lang('start_message'))
+
+
+@bot.exceptionhandler
+async def exception_handler(e: Exception, ctx: bot.Context) -> bool:
+ return False
+
+
+@bot.defaultreplymarkup
+def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
+ buttons = []
+ 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)
+
+
+devices = []
+mqtt = MqttWrapper(client_id='relay_mqtt_bot')
+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)
+
+ type_emoji = type_emojis[node_data['relay']['device_type']]
+
+ for action in UserAction:
+ messages = []
+ 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.connect_and_loop(loop_forever=False)
+
+bot.run(start_handler=start)
+
+mqtt.disconnect()
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 dc081b0..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,16 +15,15 @@ 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
)
-config.load('sensors_bot')
+config.load_app('sensors_bot')
bot.initialize()
bot.lang.ru(
@@ -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 186337a..fa22ba7 100755
--- a/src/sound_bot.py
+++ b/bin/sound_bot.py
@@ -2,32 +2,33 @@
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
from PIL import Image
-config.load('sound_bot')
+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 9d53362..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.
@@ -77,7 +78,7 @@ if __name__ == '__main__':
if not os.getegid() == 0:
raise RuntimeError("Must be run as root.")
- config.load('sound_node')
+ config.load_app('sound_node')
storage = SoundRecordStorage(config['node']['storage'])
diff --git a/src/sound_sensor_node.py b/bin/sound_sensor_node.py
index d9a8999..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__)
@@ -14,14 +15,14 @@ if __name__ == '__main__':
if not os.getegid() == 0:
sys.exit('Must be run as root.')
- config.load('sound_sensor_node')
+ config.load_app('sound_sensor_node')
kwargs = {}
if 'delay' in config['node']:
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 aa62608..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] = {}
@@ -159,10 +160,10 @@ def api_error_handler(exc, name, req: RequestParams):
if __name__ == '__main__':
- config.load('sound_sensor_server')
+ 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 3b2ba6e..d08a4f4 100755
--- a/src/ssh_tunnels_config_util.py
+++ b/bin/ssh_tunnels_config_util.py
@@ -1,14 +1,14 @@
#!/usr/bin/env python3
-
-from home.config import config
+import __py_include
+from homekit.config import config
if __name__ == '__main__':
- config.load('ssh_tunnels_config_util')
+ config.load_app('ssh_tunnels_config_util')
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/bin/temphum_mqtt_node.py b/bin/temphum_mqtt_node.py
new file mode 100755
index 0000000..9ea436d
--- /dev/null
+++ b/bin/temphum_mqtt_node.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+import asyncio
+import json
+import logging
+import __py_include
+
+from typing import Optional
+
+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
+lock = asyncio.Lock()
+delay = 0.01
+
+
+async def get_measurements():
+ async with lock:
+ await asyncio.sleep(delay)
+
+ temp = sensor.temperature()
+ rh = sensor.humidity()
+
+ return rh, temp
+
+
+async def handle_client(reader, writer):
+ request = None
+ while request != 'quit':
+ try:
+ request = await reader.read(255)
+ if request == b'\x04':
+ break
+ request = request.decode('utf-8').strip()
+ except Exception:
+ break
+
+ if request == 'read':
+ try:
+ rh, temp = await asyncio.wait_for(get_measurements(), timeout=3)
+ data = dict(humidity=rh, temp=temp)
+ except asyncio.TimeoutError as e:
+ logger.exception(e)
+ data = dict(error='i2c call timed out')
+ else:
+ data = dict(error='invalid request')
+
+ writer.write((json.dumps(data) + '\r\n').encode('utf-8'))
+ try:
+ await writer.drain()
+ except ConnectionResetError:
+ pass
+
+ writer.close()
+
+
+async def run_server(host, port):
+ server = await asyncio.start_server(handle_client, host, port)
+ async with server:
+ logger.info('Server started.')
+ await server.serve_forever()
+
+
+if __name__ == '__main__':
+ config.load_app()
+
+ if 'measure_delay' in config['sensor']:
+ delay = float(config['sensor']['measure_delay'])
+
+ sensor = create_sensor(SensorType(config['sensor']['type']),
+ int(config['sensor']['bus']))
+
+ try:
+ host, port = config.get_addr('server.listen')
+ asyncio.run(run_server(host, port))
+ except KeyboardInterrupt:
+ logging.info('Exiting...')
diff --git a/src/sensors_mqtt_receiver.py b/bin/temphum_mqtt_receiver.py
index a377ddd..d0a378e 100755
--- a/src/sensors_mqtt_receiver.py
+++ b/bin/temphum_mqtt_receiver.py
@@ -1,22 +1,13 @@
#!/usr/bin/env python3
import paho.mqtt.client as mqtt
import re
+import __py_include
-from home.mqtt import MqttBase
-from home.config import config
-from home.mqtt.payload.sensors import Temperature
-from home.api.types import TemperatureSensorLocation
-from home.database import SensorsDatabase
+from homekit.config import config
+from homekit.mqtt import MqttWrapper, MqttNode
-def get_sensor_type(sensor: str) -> TemperatureSensorLocation:
- for item in TemperatureSensorLocation:
- if sensor == item.name.lower():
- return item
- raise ValueError(f'unexpected sensor value: {sensor}')
-
-
-class MqttServer(MqttBase):
+class MqttServer(Mqtt):
def __init__(self):
super().__init__(clean_session=False)
self.database = SensorsDatabase()
@@ -47,7 +38,11 @@ class MqttServer(MqttBase):
if __name__ == '__main__':
- config.load('sensors_mqtt_receiver')
+ config.load_app('temphum_mqtt_receiver')
+
+ mqtt = MqttWrapper(clean_session=False)
+ node = MqttNode(node_id='+')
+ node.load_module('temphum', write_to_database=True)
+ mqtt.add_node(node)
- server = MqttServer()
- server.connect_and_loop()
+ mqtt.connect_and_loop() \ No newline at end of file
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 0f90835..1cfaa84 100755
--- a/src/temphum_smbus_util.py
+++ b/bin/temphum_smbus_util.py
@@ -1,6 +1,9 @@
#!/usr/bin/env python3
+import __py_include
+
from argparse import ArgumentParser
-from home.temphum import SensorType, 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 f4d1fca..9ea436d 100755
--- a/src/temphumd.py
+++ b/bin/temphumd.py
@@ -2,14 +2,16 @@
import asyncio
import json
import logging
+import __py_include
from typing import Optional
-from home.config import config
-from home.temphum import SensorType, create_sensor, TempHumSensor
+from homekit.config import config
+from homekit.temphum import SensorType, BaseSensor
+from homekit.temphum.i2c import create_sensor
logger = logging.getLogger(__name__)
-sensor: Optional[TempHumSensor] = None
+sensor: Optional[BaseSensor] = None
lock = asyncio.Lock()
delay = 0.01
@@ -62,7 +64,7 @@ async def run_server(host, port):
if __name__ == '__main__':
- config.load()
+ config.load_app()
if 'measure_delay' in config['sensor']:
delay = float(config['sensor']['measure_delay'])
diff --git a/src/web_api.py b/bin/web_api.py
index 0ddc6bd..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()
@@ -231,7 +207,7 @@ if __name__ == '__main__':
_app_name = 'web_api'
if is_development_mode():
_app_name += '_dev'
- config.load(_app_name)
+ config.load_app(_app_name)
loop = asyncio.get_event_loop()
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 fd08925..816c764 100644
--- a/platformio/common/libs/main/homekit/main.cpp
+++ b/include/pio/libs/main/homekit/main.cpp
@@ -6,7 +6,12 @@
namespace homekit::main {
+#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
enum WorkingMode working_mode = WorkingMode::NORMAL;
+#endif
+#endif
+
static const uint16_t recovery_boot_detection_ms = 2000;
static const uint8_t recovery_boot_delay_ms = 100;
@@ -22,8 +27,10 @@ static StopWatch blinkStopWatch;
#endif
#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
static DNSServer* dnsServer = nullptr;
#endif
+#endif
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
@@ -45,6 +52,7 @@ static void wifiConnect() {
}
#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
static void wifiHotspot() {
led::mcu_led->on();
@@ -71,13 +79,16 @@ static void waitForRecoveryPress() {
}
}
#endif
+#endif
void setup() {
WiFi.disconnect();
+#ifndef CONFIG_NO_RECOVERY
#ifndef CONFIG_TARGET_ESP01
homekit::main::waitForRecoveryPress();
#endif
+#endif
#ifdef DEBUG
Serial.begin(115200);
@@ -95,6 +106,7 @@ void setup() {
}
#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
switch (working_mode) {
case WorkingMode::RECOVERY:
wifiHotspot();
@@ -102,19 +114,24 @@ void setup() {
case WorkingMode::NORMAL:
#endif
+#endif
wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected);
wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected);
wifiConnect();
+#ifndef CONFIG_NO_RECOVERY
#ifndef CONFIG_TARGET_ESP01
break;
}
#endif
+#endif
}
void loop(LoopConfig* config) {
+#ifndef CONFIG_NO_RECOVERY
#ifndef CONFIG_TARGET_ESP01
if (working_mode == WorkingMode::NORMAL) {
#endif
+#endif
if (wifi_state == WiFiConnectionState::WAITING) {
PRINT(".");
led::mcu_led->blink(2, 50);
@@ -166,6 +183,7 @@ void loop(LoopConfig* config) {
}
#endif
}
+#ifndef CONFIG_NO_RECOVERY
#ifndef CONFIG_TARGET_ESP01
} else {
if (dnsServer != nullptr)
@@ -176,6 +194,7 @@ void loop(LoopConfig* config) {
httpServer->loop();
}
#endif
+#endif
}
static void onWifiConnected(const WiFiEventStationModeGotIP& event) {
@@ -191,4 +210,4 @@ static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) {
wifiTimer.once(2, wifiConnect);
}
-} \ No newline at end of file
+}
diff --git a/platformio/common/libs/main/homekit/main.h b/include/pio/libs/main/homekit/main.h
index a503dd0..78a0695 100644
--- a/platformio/common/libs/main/homekit/main.h
+++ b/include/pio/libs/main/homekit/main.h
@@ -10,8 +10,10 @@
#include <homekit/config.h>
#include <homekit/logging.h>
#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
#include <homekit/http_server.h>
#endif
+#endif
#include <homekit/wifi.h>
#include <homekit/mqtt/mqtt.h>
@@ -20,6 +22,7 @@
namespace homekit::main {
#ifndef CONFIG_TARGET_ESP01
+#ifndef CONFIG_NO_RECOVERY
enum class WorkingMode {
RECOVERY, // AP mode, http server with configuration
NORMAL, // MQTT client
@@ -27,6 +30,7 @@ enum class WorkingMode {
extern enum WorkingMode working_mode;
#endif
+#endif
enum class WiFiConnectionState {
WAITING = 0,
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 e78ff12..0ac7637 100644
--- a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp
+++ b/include/pio/libs/mqtt/homekit/mqtt/module.cpp
@@ -21,6 +21,6 @@ void MqttModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, con
void MqttModule::handleOnPublish(uint16_t packetId) {}
-void MqttModule::handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) {}
+void MqttModule::onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) {}
}
diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.h b/include/pio/libs/mqtt/homekit/mqtt/module.h
index e4a01f8..0a328f3 100644
--- a/platformio/common/libs/mqtt/homekit/mqtt/module.h
+++ b/include/pio/libs/mqtt/homekit/mqtt/module.h
@@ -28,20 +28,25 @@ public:
, receiveOnPublish(_receiveOnPublish)
, receiveOnDisconnect(_receiveOnDisconnect) {}
- virtual void init(Mqtt& mqtt) = 0;
virtual void tick(Mqtt& mqtt) = 0;
+ virtual void onConnect(Mqtt& mqtt) = 0;
+ virtual void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason);
+
virtual void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total);
virtual void handleOnPublish(uint16_t packetId);
- virtual void handleOnDisconnect(espMqttClientTypes::DisconnectReason reason);
inline void setInitialized() {
initialized = true;
}
- inline short getTickInterval() {
- return tickInterval;
- }
+ inline void unsetInitialized() {
+ initialized = false;
+ }
+
+ inline short getTickInterval() const {
+ return tickInterval;
+ }
friend class Mqtt;
};
diff --git a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp
index cb2cea7..aa769a5 100644
--- a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp
+++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp
@@ -34,7 +34,7 @@ Mqtt::Mqtt() {
for (auto* module: modules) {
if (!module->initialized) {
- module->init(*this);
+ module->onConnect(*this);
module->setInitialized();
}
}
@@ -50,18 +50,13 @@ Mqtt::Mqtt() {
#endif
for (auto* module: modules) {
- if (module->receiveOnDisconnect) {
- module->handleOnDisconnect(reason);
- }
+ module->onDisconnect(*this, reason);
+ module->unsetInitialized();
}
-// if (ota.readyToRestart) {
-// restartTimer.once(1, restart);
-// } else {
- reconnectTimer.once(2, [&]() {
- reconnect();
- });
-// }
+ reconnectTimer.once(2, [&]() {
+ reconnect();
+ });
});
client.onSubscribe([&](uint16_t packetId, const SubscribeReturncode* returncodes, size_t len) {
@@ -79,7 +74,7 @@ Mqtt::Mqtt() {
PRINTF("mqtt: message received, topic=%s, qos=%d, dup=%d, retain=%d, len=%ul, index=%ul, total=%ul\n",
topic, properties.qos, (int)properties.dup, (int)properties.retain, len, index, total);
- const char *ptr = topic + nodeId.length() + 10;
+ const char *ptr = topic + nodeId.length() + 4;
String relevantTopic(ptr);
auto it = moduleSubscriptions.find(relevantTopic);
@@ -87,7 +82,7 @@ Mqtt::Mqtt() {
auto module = it->second;
module->handlePayload(*this, relevantTopic, properties.packetId, payload, len, index, total);
} else {
- PRINTF("error: module subscription for topic %s not found\n", topic);
+ PRINTF("error: module subscription for topic %s not found\n", relevantTopic.c_str());
}
});
@@ -130,8 +125,8 @@ void Mqtt::disconnect() {
void Mqtt::loop() {
client.loop();
for (auto& module: modules) {
- if (module->getTickInterval() != 0)
- module->tick(*this);
+ if (module->getTickInterval() != 0)
+ module->tick(*this);
}
}
@@ -154,14 +149,14 @@ uint16_t Mqtt::subscribe(const String& topic, uint8_t qos) {
void Mqtt::addModule(MqttModule* module) {
modules.emplace_back(module);
if (connected) {
- module->init(*this);
+ module->onConnect(*this);
module->setInitialized();
}
}
void Mqtt::subscribeModule(String& topic, MqttModule* module, uint8_t qos) {
moduleSubscriptions[topic] = module;
- subscribe(topic, qos);
+ subscribe(topic, qos);
}
}
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 d1ad420..f3f2504 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.9",
+ "version": "1.0.11",
"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 d36a7e9..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
@@ -7,12 +7,21 @@ namespace homekit::mqtt {
static const char TOPIC_DIAGNOSTICS[] = "diag";
static const char TOPIC_INITIAL_DIAGNOSTICS[] = "d1ag";
-void MqttDiagnosticsModule::init(Mqtt& mqtt) {}
+void MqttDiagnosticsModule::onConnect(Mqtt &mqtt) {
+ sendDiagnostics(mqtt);
+}
+
+void MqttDiagnosticsModule::onDisconnect(Mqtt &mqtt, espMqttClientTypes::DisconnectReason reason) {
+ initialSent = false;
+}
void MqttDiagnosticsModule::tick(Mqtt& mqtt) {
if (!tickElapsed())
return;
+ sendDiagnostics(mqtt);
+}
+void MqttDiagnosticsModule::sendDiagnostics(Mqtt& mqtt) {
auto cfg = config::read();
if (!initialSent) {
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 055c179..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
@@ -32,12 +32,15 @@ class MqttDiagnosticsModule: public MqttModule {
private:
bool initialSent;
+ void sendDiagnostics(Mqtt& mqtt);
+
public:
MqttDiagnosticsModule()
: MqttModule(30)
, initialSent(false) {}
- void init(Mqtt& mqtt) override;
+ void onConnect(Mqtt& mqtt) override;
+ void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override;
void tick(Mqtt& mqtt) override;
};
diff --git a/platformio/common/libs/mqtt_module_diagnostics/library.json b/include/pio/libs/mqtt_module_diagnostics/library.json
index 8df306d..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.1",
+ "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 2f5f814..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
@@ -12,7 +12,7 @@ using homekit::led::mcu_led;
static const char TOPIC_OTA[] = "ota";
static const char TOPIC_OTA_RESPONSE[] = "otares";
-void MqttOtaModule::init(Mqtt& mqtt) {
+void MqttOtaModule::onConnect(Mqtt& mqtt) {
String topic(TOPIC_OTA);
mqtt.subscribeModule(topic, this);
}
@@ -140,17 +140,15 @@ uint16_t MqttOtaModule::sendResponse(Mqtt& mqtt, OtaResult status, uint8_t error
return mqtt.publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp));
}
-void MqttOtaModule::handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) {
- if (ota.started()) {
+void MqttOtaModule::onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) {
+ if (ota.readyToRestart) {
+ restartTimer.once(1, restart);
+ } else if (ota.started()) {
PRINTLN("mqtt: update was in progress, canceling..");
ota.clean();
Update.end();
Update.clearError();
}
-
- if (ota.readyToRestart) {
- restartTimer.once(1, restart);
- }
}
void MqttOtaModule::handleOnPublish(uint16_t packetId) {
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 53613c3..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
@@ -57,11 +57,14 @@ private:
public:
MqttOtaModule() : MqttModule(0, true, true) {}
- void init(Mqtt& mqtt) override;
+ void onConnect(Mqtt& mqtt) override;
+ void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override;
+
void tick(Mqtt& mqtt) override;
+
void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override;
void handleOnPublish(uint16_t packetId) override;
- void handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) override;
+
inline bool isReadyToRestart() const {
return ota.readyToRestart;
}
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 ab40727..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
@@ -5,19 +5,28 @@
namespace homekit::mqtt {
static const char TOPIC_RELAY_SWITCH[] = "relay/switch";
+static const char TOPIC_RELAY_STATUS[] = "relay/status";
-void MqttRelayModule::init(Mqtt &mqtt) {
- String topic(TOPIC_RELAY_SWITCH);
- mqtt.subscribeModule(topic, this, 1);
+void MqttRelayModule::onConnect(Mqtt &mqtt) {
+ String topic(TOPIC_RELAY_SWITCH);
+ mqtt.subscribeModule(topic, this, 1);
+}
+
+void MqttRelayModule::onDisconnect(Mqtt &mqtt, espMqttClientTypes::DisconnectReason reason) {
+#ifdef CONFIG_RELAY_OFF_ON_DISCONNECT
+ if (relay::state()) {
+ relay::off();
+ }
+#endif
}
void MqttRelayModule::tick(homekit::mqtt::Mqtt& mqtt) {}
void MqttRelayModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) {
- if (topic != TOPIC_RELAY_SWITCH)
- return;
+ if (topic != TOPIC_RELAY_SWITCH)
+ return;
- if (length != sizeof(MqttRelaySwitchPayload)) {
+ if (length != sizeof(MqttRelaySwitchPayload)) {
PRINTF("error: size of payload (%ul) does not match expected (%ul)\n",
length, sizeof(MqttRelaySwitchPayload));
return;
@@ -29,6 +38,8 @@ void MqttRelayModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId
return;
}
+ MqttRelayStatusPayload resp{};
+
if (pd->state == 1) {
PRINTLN("mqtt: turning relay on");
relay::on();
@@ -38,6 +49,10 @@ void MqttRelayModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId
} else {
PRINTLN("error: unexpected state value");
}
+
+ resp.opened = relay::state();
+ mqtt.publish(TOPIC_RELAY_STATUS, reinterpret_cast<uint8_t*>(&resp), sizeof(resp));
}
}
+
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 6420de1..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
@@ -10,14 +10,20 @@ struct MqttRelaySwitchPayload {
uint8_t state;
} __attribute__((packed));
+struct MqttRelayStatusPayload {
+ uint8_t opened;
+} __attribute__((packed));
+
class MqttRelayModule : public MqttModule {
public:
MqttRelayModule() : MqttModule(0) {}
- void init(Mqtt& mqtt) override;
+ void onConnect(Mqtt& mqtt) override;
+ void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override;
void tick(Mqtt& mqtt) override;
- void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override;
+ void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override;
};
}
#endif //HOMEKIT_LIB_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 82f1d74..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
@@ -4,7 +4,7 @@ namespace homekit::mqtt {
static const char TOPIC_TEMPHUM_DATA[] = "temphum/data";
-void MqttTemphumModule::init(Mqtt &mqtt) {}
+void MqttTemphumModule::onConnect(Mqtt &mqtt) {}
void MqttTemphumModule::tick(homekit::mqtt::Mqtt& mqtt) {
if (!tickElapsed())
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 5c41cef..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
@@ -19,7 +19,7 @@ private:
public:
MqttTemphumModule(temphum::Sensor* _sensor) : MqttModule(10), sensor(_sensor) {}
- void init(Mqtt& mqtt) override;
+ void onConnect(Mqtt& mqtt) override;
void tick(Mqtt& mqtt) override;
};
diff --git a/include/pio/libs/mqtt_module_temphum/library.json b/include/pio/libs/mqtt_module_temphum/library.json
new file mode 100644
index 0000000..c7ee7af
--- /dev/null
+++ b/include/pio/libs/mqtt_module_temphum/library.json
@@ -0,0 +1,11 @@
+{
+ "name": "homekit_mqtt_module_temphum",
+ "version": "1.0.10",
+ "build": {
+ "flags": "-I../../include"
+ },
+ "dependencies": {
+ "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..329b7ca 100644
--- a/platformio/common/libs/temphum/library.json
+++ b/include/pio/libs/temphum/library.json
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 53e6bce..8ed754b 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..c7dbc38
--- /dev/null
+++ b/include/py/homekit/camera/config.py
@@ -0,0 +1,130 @@
+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 {
+ 'cams': {
+ 'type': 'dict',
+ 'keysrules': {'type': ['string', 'integer']},
+ 'valuesrules': {
+ 'type': 'dict',
+ 'schema': {
+ 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True},
+ 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True},
+ 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True},
+ 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True},
+ 'disk': {'type': 'integer', 'required': True},
+ 'motion': {
+ 'type': 'dict',
+ 'schema': {
+ 'threshold': {'type': ['float', 'integer']},
+ 'roi': {
+ 'type': 'list',
+ 'schema': {'type': 'string', 'check_with': _validate_roi_line}
+ }
+ }
+ },
+ 'rtsp_tcp': {'type': 'boolean'}
+ }
+ }
+ },
+ '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},
+ }
+ }
+ }
+
+ @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'}
+ }
+ }
+
+ 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, cam: int) -> VideoContainerType:
+ return VideoContainerType(self['cams'][cam]['container'])
+
+ def get_camera_type(self, cam: int) -> CameraType:
+ return CameraType(self['cams'][cam]['type'])
+
+ def get_rtsp_creds(self) -> tuple[str, str]:
+ return self['rtsp_creds']['login'], self['rtsp_creds']['password']
+
+ def should_use_tcp_for_rtsp(self, cam: int) -> bool:
+ return 'rtsp_tcp' in self['cams'][cam] and self['cams'][cam]['rtsp_tcp']
+
+ def get_camera_ip(self, camera: int) -> str:
+ return f'192.168.5.{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..c313b58
--- /dev/null
+++ b/include/py/homekit/camera/types.py
@@ -0,0 +1,46 @@
+from enum import Enum
+
+
+class CameraType(Enum):
+ ESP32 = 'esp32'
+ ALIEXPRESS_NONAME = 'ali'
+ HIKVISION = 'hik'
+
+ 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 == CameraType.HIKVISION:
+ return '/Streaming/Channels/2'
+ elif self.value == CameraType.ALIEXPRESS_NONAME:
+ return '/?stream=1.sdp'
+ else:
+ raise ValueError(f'unsupported camera type {self.value}')
+
+
+class VideoContainerType(Enum):
+ MP4 = 'mp4'
+ MOV = 'mov'
+
+
+class VideoCodecType(Enum):
+ H264 = 'h264'
+ H265 = 'h265'
+
+
+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/include/py/homekit/config/__init__.py b/include/py/homekit/config/__init__.py
new file mode 100644
index 0000000..8fedfa6
--- /dev/null
+++ b/include/py/homekit/config/__init__.py
@@ -0,0 +1,14 @@
+from .config import (
+ Config,
+ ConfigUnit,
+ AppConfigUnit,
+ Translation,
+ config,
+ is_development_mode,
+ setup_logging,
+ CONFIG_DIRECTORIES
+)
+from ._configs import (
+ LinuxBoardsConfig,
+ ServicesListConfig
+) \ No newline at end of file
diff --git a/include/py/homekit/config/_configs.py b/include/py/homekit/config/_configs.py
new file mode 100644
index 0000000..f88c8ea
--- /dev/null
+++ b/include/py/homekit/config/_configs.py
@@ -0,0 +1,61 @@
+from .config import ConfigUnit
+from typing import Optional
+
+
+class ServicesListConfig(ConfigUnit):
+ NAME = 'services_list'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'type': 'list',
+ 'empty': False,
+ 'schema': {
+ 'type': 'string'
+ }
+ }
+
+
+class LinuxBoardsConfig(ConfigUnit):
+ NAME = 'linux_boards'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'type': 'dict',
+ 'schema': {
+ 'mdns': {'type': 'string', 'required': True},
+ 'board': {'type': 'string', 'required': True},
+ 'network': {
+ 'type': 'list',
+ 'required': True,
+ 'empty': False,
+ 'allowed': ['wifi', 'ethernet']
+ },
+ 'ram': {'type': 'integer', 'required': True},
+ 'online': {'type': 'boolean', 'required': True},
+
+ # optional
+ 'services': {
+ 'type': 'list',
+ 'empty': False,
+ 'allowed': ServicesListConfig().get()
+ },
+ 'ext_hdd': {
+ 'type': 'list',
+ 'schema': {
+ 'type': 'dict',
+ 'schema': {
+ 'mountpoint': {'type': 'string', 'required': True},
+ 'size': {'type': 'integer', 'required': True}
+ }
+ },
+ },
+ }
+ }
+
+ 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/include/py/homekit/config/config.py b/include/py/homekit/config/config.py
new file mode 100644
index 0000000..5fe1ae8
--- /dev/null
+++ b/include/py/homekit/config/config.py
@@ -0,0 +1,406 @@
+import yaml
+import logging
+import os
+import cerberus
+import cerberus.errors
+
+from abc import ABC
+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 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,), ())
+
+
+CONFIG_DIRECTORIES = (
+ join(os.environ['HOME'], '.config', 'homekit'),
+ '/etc/homekit'
+)
+
+
+class RootSchemaType(Enum):
+ DEFAULT = auto()
+ DICT = auto()
+ LIST = auto()
+
+
+class BaseConfigUnit(ABC):
+ _data: MutableMapping[str, Any]
+ _logger: logging.Logger
+
+ def __init__(self):
+ self._data = {}
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ raise NotImplementedError('overwriting config values is prohibited')
+
+ def __contains__(self, key):
+ return key in self._data
+
+ 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,
+ default=None):
+ if key is None:
+ return self._data
+
+ cur = self._data
+ pts = key.split('.')
+ for i in range(len(pts)):
+ k = pts[i]
+ if i < len(pts)-1:
+ if k not in cur:
+ raise KeyError(f'key {k} not found')
+ else:
+ return cur[k] if k in cur else default
+ cur = self._data[k]
+
+ raise KeyError(f'option {key} not found')
+
+
+class ConfigUnit(BaseConfigUnit):
+ NAME = 'dumb'
+
+ _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' and load:
+ self.load_from(self.get_config_path())
+ self.validate()
+
+ elif name is not None:
+ self.NAME = name
+
+ @classmethod
+ def get_config_path(cls, name=None) -> str:
+ if name is None:
+ name = cls.NAME
+ if name is None:
+ raise ValueError('get_config_path: name is none')
+
+ for dirname in CONFIG_DIRECTORIES:
+ if isdir(dirname):
+ filename = join(dirname, f'{name}.yaml')
+ if isfile(filename):
+ return filename
+
+ raise IOError(f'\'{name}.yaml\' not found')
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return None
+
+ @classmethod
+ def _addr_schema(cls, required=False, **kwargs):
+ return {
+ 'type': 'addr',
+ 'coerce': Addr.fromstring,
+ 'required': required,
+ **kwargs
+ }
+
+ def validate(self):
+ schema = self.schema()
+ if not schema:
+ self._logger.warning('validate: no schema')
+ return
+
+ if isinstance(self, AppConfigUnit):
+ schema['logging'] = {
+ 'type': 'dict',
+ 'schema': {
+ 'verbose': {'type': 'boolean'}
+ }
+ }
+
+ rst = RootSchemaType.DEFAULT
+ try:
+ if schema['type'] == 'dict':
+ 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()
+
+ if rst == RootSchemaType.DICT:
+ normalized = v.validated({'document': self._data},
+ {'document': {
+ 'type': 'dict',
+ 'keysrules': {'type': 'string'},
+ 'valuesrules': schema
+ }})['document']
+ elif rst == RootSchemaType.LIST:
+ v = MyValidator()
+ normalized = v.validated({'document': self._data}, {'document': schema})['document']
+ else:
+ normalized = v.validated(self._data, schema)
+
+ if not normalized:
+ raise cerberus.DocumentError(f'validation failed: {v.errors}')
+
+ self._data = normalized
+
+ try:
+ self.custom_validator(self._data)
+ except Exception as e:
+ raise cerberus.DocumentError(f'{self.__class__.__name__}: {str(e)}')
+
+ @staticmethod
+ def custom_validator(data):
+ pass
+
+ def get_addr(self, key: str):
+ return Addr.fromstring(self.get(key))
+
+
+class AppConfigUnit(ConfigUnit):
+ _logging_verbose: bool
+ _logging_fmt: Optional[str]
+ _logging_file: Optional[str]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(load=False, *args, **kwargs)
+ self._logging_verbose = False
+ self._logging_fmt = None
+ self._logging_file = None
+
+ def logging_set_fmt(self, fmt: str) -> None:
+ self._logging_fmt = fmt
+
+ def logging_get_fmt(self) -> Optional[str]:
+ try:
+ return self['logging']['default_fmt']
+ except (KeyError, TypeError):
+ return self._logging_fmt
+
+ def logging_set_file(self, file: str) -> None:
+ self._logging_file = file
+
+ def logging_get_file(self) -> Optional[str]:
+ try:
+ return self['logging']['file']
+ except (KeyError, TypeError):
+ return self._logging_file
+
+ def logging_set_verbose(self):
+ self._logging_verbose = True
+
+ def logging_is_verbose(self) -> bool:
+ try:
+ return bool(self['logging']['verbose'])
+ except (KeyError, TypeError):
+ return self._logging_verbose
+
+
+class TranslationUnit(BaseConfigUnit):
+ pass
+
+
+class Translation:
+ LANGUAGES = ('en', 'ru')
+ _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:
+ app_name: Optional[str]
+ app_config: AppConfigUnit
+
+ def __init__(self):
+ self.app_name = None
+ self.app_config = AppConfigUnit()
+
+ def load_app(self,
+ name: Optional[Union[str, AppConfigUnit, bool]] = None,
+ use_cli=True,
+ parser: ArgumentParser = None,
+ no_config=False):
+ global app_config
+
+ 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
+ print(self.app_config)
+ self.app_config = name()
+ print(self.app_config)
+ app_config = self.app_config
+ else:
+ self.app_name = name if isinstance(name, str) else None
+
+ if self.app_name is None and not use_cli:
+ raise RuntimeError('either config name must be none or use_cli must be True')
+
+ no_config = name is False or no_config
+ path = None
+
+ if use_cli:
+ if parser is None:
+ parser = ArgumentParser()
+ if not no_config:
+ parser.add_argument('-c', '--config', type=str, required=name is None,
+ help='Path to the config in TOML or YAML format')
+ parser.add_argument('-V', '--verbose', action='store_true')
+ parser.add_argument('--log-file', type=str)
+ parser.add_argument('--log-default-fmt', action='store_true')
+ args = parser.parse_args()
+
+ if not no_config and args.config:
+ path = args.config
+
+ if args.verbose:
+ self.app_config.logging_set_verbose()
+ if args.log_file:
+ self.app_config.logging_set_file(args.log_file)
+ if args.log_default_fmt:
+ self.app_config.logging_set_fmt(args.log_default_fmt)
+
+ if not isinstance(name, ConfigUnit):
+ if not no_config and path is None:
+ 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(),
+ self.app_config.logging_get_fmt())
+
+ if use_cli:
+ return args
+
+
+config = Config()
+
+
+def is_development_mode() -> bool:
+ if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev':
+ return True
+
+ return ('logging' in config.app_config) and ('verbose' in config.app_config['logging']) and (config.app_config['logging']['verbose'] is True)
+
+
+def setup_logging(verbose=False, log_file=None, default_fmt=None):
+ logging_level = logging.INFO
+ if is_development_mode() or verbose:
+ logging_level = logging.DEBUG
+ _add_logging_level('TRACE', logging.DEBUG-5)
+
+ log_config = {'level': logging_level}
+ if not default_fmt:
+ log_config['format'] = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+
+ if log_file is not None:
+ log_config['filename'] = log_file
+ log_config['encoding'] = 'utf-8'
+
+ logging.basicConfig(**log_config)
+
+
+# https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
+def _add_logging_level(levelName, levelNum, methodName=None):
+ """
+ Comprehensively adds a new logging level to the `logging` module and the
+ currently configured logging class.
+
+ `levelName` becomes an attribute of the `logging` module with the value
+ `levelNum`. `methodName` becomes a convenience method for both `logging`
+ itself and the class returned by `logging.getLoggerClass()` (usually just
+ `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
+ used.
+
+ To avoid accidental clobberings of existing attributes, this method will
+ raise an `AttributeError` if the level name is already an attribute of the
+ `logging` module or if the method name is already present
+
+ Example
+ -------
+ >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
+ >>> logging.getLogger(__name__).setLevel("TRACE")
+ >>> logging.getLogger(__name__).trace('that worked')
+ >>> logging.trace('so did this')
+ >>> logging.TRACE
+ 5
+
+ """
+ if not methodName:
+ methodName = levelName.lower()
+
+ if hasattr(logging, levelName):
+ raise AttributeError('{} already defined in logging module'.format(levelName))
+ if hasattr(logging, methodName):
+ raise AttributeError('{} already defined in logging module'.format(methodName))
+ if hasattr(logging.getLoggerClass(), methodName):
+ raise AttributeError('{} already defined in logger class'.format(methodName))
+
+ # This method was inspired by the answers to Stack Overflow post
+ # http://stackoverflow.com/q/2183233/2988730, especially
+ # http://stackoverflow.com/a/13638084/2988730
+ def logForLevel(self, message, *args, **kwargs):
+ if self.isEnabledFor(levelNum):
+ self._log(levelNum, message, args, **kwargs)
+ def logToRoot(message, *args, **kwargs):
+ logging.log(levelNum, message, *args, **kwargs)
+
+ logging.addLevelName(levelNum, levelName)
+ setattr(logging, levelName, levelNum)
+ setattr(logging.getLoggerClass(), methodName, logForLevel)
+ setattr(logging, methodName, logToRoot) \ No newline at end of file
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 ca81628..d0ec283 100644
--- a/src/home/database/clickhouse.py
+++ b/include/py/homekit/database/clickhouse.py
@@ -1,7 +1,7 @@
import logging
from zoneinfo import ZoneInfo
-from datetime import datetime, timedelta
+from datetime import datetime
from clickhouse_driver import Client as ClickhouseClient
from ..config import is_development_mode
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/src/home/http/__init__.py b/include/py/homekit/http/__init__.py
index 6030e95..6030e95 100644
--- a/src/home/http/__init__.py
+++ b/include/py/homekit/http/__init__.py
diff --git a/src/home/http/http.py b/include/py/homekit/http/http.py
index 3e70751..3e70751 100644
--- a/src/home/http/http.py
+++ b/include/py/homekit/http/http.py
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..e284dfe
--- /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': {'type': 'string'},
+ 'local_addr': {'type': 'string'},
+ } \ No newline at end of file
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 976c990..6923105 100644
--- a/src/home/media/__init__.py
+++ b/include/py/homekit/media/__init__.py
@@ -12,6 +12,7 @@ __map__ = {
__all__ = list(itertools.chain(*__map__.values()))
+
def __getattr__(name):
if name in __all__:
for file, names in __map__.items():
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/mqtt/__init__.py b/include/py/homekit/mqtt/__init__.py
new file mode 100644
index 0000000..707d59c
--- /dev/null
+++ b/include/py/homekit/mqtt/__init__.py
@@ -0,0 +1,7 @@
+from ._mqtt import Mqtt
+from ._node import MqttNode
+from ._module import MqttModule
+from ._wrapper import MqttWrapper
+from ._config import MqttConfig, MqttCreds, MqttNodesConfig
+from ._payload import MqttPayload, MqttPayloadCustomField
+from ._util import get_modules as get_mqtt_modules \ No newline at end of file
diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py
new file mode 100644
index 0000000..9ba9443
--- /dev/null
+++ b/include/py/homekit/mqtt/_config.py
@@ -0,0 +1,165 @@
+from ..config import ConfigUnit
+from typing import Optional, Union
+from ..util import Addr
+from collections import namedtuple
+
+MqttCreds = namedtuple('MqttCreds', 'username, password')
+
+
+class MqttConfig(ConfigUnit):
+ NAME = 'mqtt'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ addr_schema = {
+ 'type': 'dict',
+ 'required': True,
+ 'schema': {
+ 'host': {'type': 'string', 'required': True},
+ 'port': {'type': 'integer', 'required': True}
+ }
+ }
+
+ schema = {}
+ for key in ('local', 'remote'):
+ schema[f'{key}_addr'] = addr_schema
+
+ schema['creds'] = {
+ 'type': 'dict',
+ 'required': True,
+ 'keysrules': {'type': 'string'},
+ 'valuesrules': {
+ 'type': 'dict',
+ 'schema': {
+ 'username': {'type': 'string', 'required': True},
+ 'password': {'type': 'string', 'required': True},
+ }
+ }
+ }
+
+ for key in ('client', 'server'):
+ schema[f'default_{key}_creds'] = {'type': 'string', 'required': True}
+
+ return schema
+
+ def remote_addr(self) -> Addr:
+ return Addr(host=self['remote_addr']['host'],
+ port=self['remote_addr']['port'])
+
+ def local_addr(self) -> Addr:
+ return Addr(host=self['local_addr']['host'],
+ port=self['local_addr']['port'])
+
+ def creds_by_name(self, name: str) -> MqttCreds:
+ return MqttCreds(username=self['creds'][name]['username'],
+ password=self['creds'][name]['password'])
+
+ def creds(self) -> MqttCreds:
+ return self.creds_by_name(self['default_client_creds'])
+
+ def server_creds(self) -> MqttCreds:
+ return self.creds_by_name(self['default_server_creds'])
+
+
+class MqttNodesConfig(ConfigUnit):
+ NAME = 'mqtt_nodes'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'common': {
+ 'type': 'dict',
+ 'schema': {
+ 'temphum': {
+ 'type': 'dict',
+ 'schema': {
+ 'interval': {'type': 'integer'}
+ }
+ },
+ 'password': {'type': 'string'}
+ }
+ },
+ 'nodes': {
+ 'type': 'dict',
+ 'required': True,
+ 'keysrules': {'type': 'string'},
+ 'valuesrules': {
+ 'type': 'dict',
+ 'schema': {
+ 'type': {'type': 'string', 'required': True, 'allowed': ['esp8266', 'linux', 'none'],},
+ 'board': {'type': 'string', 'allowed': ['nodemcu', 'd1_mini_lite', 'esp12e']},
+ 'temphum': {
+ 'type': 'dict',
+ 'schema': {
+ 'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']},
+ 'interval': {'type': 'integer'},
+ 'i2c_bus': {'type': 'integer'},
+ 'tcpserver': {
+ 'type': 'dict',
+ 'schema': {
+ 'port': {'type': 'integer', 'required': True}
+ }
+ }
+ }
+ },
+ 'relay': {
+ 'type': 'dict',
+ 'schema': {
+ 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid'], 'required': True},
+ 'legacy_topics': {'type': 'boolean'}
+ }
+ },
+ 'password': {'type': 'string'}
+ }
+ }
+ }
+ }
+
+ @staticmethod
+ def custom_validator(data):
+ for name, node in data['nodes'].items():
+ if 'temphum' in node:
+ if node['type'] == 'linux':
+ if 'i2c_bus' not in node['temphum']:
+ raise KeyError(f'nodes.{name}.temphum: i2c_bus is missing but required for type=linux')
+ if node['type'] in ('esp8266',) and 'board' not in node:
+ raise KeyError(f'nodes.{name}: board is missing but required for type={node["type"]}')
+
+ def get_node(self, name: str) -> dict:
+ node = self['nodes'][name]
+ if node['type'] == 'none':
+ return node
+
+ try:
+ if 'password' not in node:
+ node['password'] = self['common']['password']
+ except KeyError:
+ pass
+
+ try:
+ if 'temphum' in node:
+ for ckey, cval in self['common']['temphum'].items():
+ if ckey not in node['temphum']:
+ node['temphum'][ckey] = cval
+ except KeyError:
+ pass
+
+ return node
+
+ def get_nodes(self,
+ filters: Optional[Union[list[str], tuple[str]]] = None,
+ only_names=False) -> Union[dict, list[str]]:
+ if filters:
+ for f in filters:
+ if f not in ('temphum', 'relay'):
+ raise ValueError(f'{self.__class__.__name__}::get_node(): invalid filter {f}')
+ reslist = []
+ resdict = {}
+ for name in self['nodes'].keys():
+ node = self.get_node(name)
+ if (not filters) or ('temphum' in filters and 'temphum' in node) or ('relay' in filters and 'relay' in node):
+ if only_names:
+ reslist.append(name)
+ else:
+ resdict[name] = node
+ return reslist if only_names else resdict
diff --git a/include/py/homekit/mqtt/_module.py b/include/py/homekit/mqtt/_module.py
new file mode 100644
index 0000000..80f27bb
--- /dev/null
+++ b/include/py/homekit/mqtt/_module.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import abc
+import logging
+import threading
+
+from time import sleep
+from ..util import next_tick_gen
+
+from typing import TYPE_CHECKING, Optional
+if TYPE_CHECKING:
+ from ._node import MqttNode
+ from ._payload import MqttPayload
+
+
+class MqttModule(abc.ABC):
+ _tick_interval: int
+ _initialized: bool
+ _connected: bool
+ _ticker: Optional[threading.Thread]
+ _mqtt_node_ref: Optional[MqttNode]
+
+ def __init__(self, tick_interval=0):
+ self._tick_interval = tick_interval
+ self._initialized = False
+ self._ticker = None
+ self._logger = logging.getLogger(self.__class__.__name__)
+ self._connected = False
+ self._mqtt_node_ref = None
+
+ def on_connect(self, mqtt: MqttNode):
+ self._connected = True
+ self._mqtt_node_ref = mqtt
+ if self._tick_interval:
+ self._start_ticker()
+
+ def on_disconnect(self, mqtt: MqttNode):
+ self._connected = False
+ self._mqtt_node_ref = None
+
+ def is_initialized(self):
+ return self._initialized
+
+ def set_initialized(self):
+ self._initialized = True
+
+ def unset_initialized(self):
+ self._initialized = False
+
+ def tick(self):
+ pass
+
+ def _tick(self):
+ g = next_tick_gen(self._tick_interval)
+ while self._connected:
+ sleep(next(g))
+ if not self._connected:
+ break
+ self.tick()
+
+ def _start_ticker(self):
+ if not self._ticker or not self._ticker.is_alive():
+ name_part = f'{self._mqtt_node_ref.id}/' if self._mqtt_node_ref else ''
+ self._ticker = None
+ self._ticker = threading.Thread(target=self._tick,
+ name=f'mqtt:{self.__class__.__name__}/{name_part}ticker')
+ self._ticker.start()
+
+ def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]:
+ pass
diff --git a/src/home/mqtt/mqtt.py b/include/py/homekit/mqtt/_mqtt.py
index 4acd4f6..47ee9ae 100644
--- a/src/home/mqtt/mqtt.py
+++ b/include/py/homekit/mqtt/_mqtt.py
@@ -3,19 +3,24 @@ import paho.mqtt.client as mqtt
import ssl
import logging
-from typing import Tuple
-from ..config import config
+from ._config import MqttCreds, MqttConfig
+from typing import Optional
-def username_and_password() -> Tuple[str, str]:
- username = config['mqtt']['username'] if 'username' in config['mqtt'] else None
- password = config['mqtt']['password'] if 'password' in config['mqtt'] else None
- return username, password
+class Mqtt:
+ _connected: bool
+ _is_server: bool
+ _mqtt_config: MqttConfig
+ def __init__(self,
+ clean_session=True,
+ client_id='',
+ creds: Optional[MqttCreds] = None,
+ is_server=False):
+ if not client_id:
+ raise ValueError('client_id must not be empty')
-class MqttBase:
- def __init__(self, clean_session=True):
- self._client = mqtt.Client(client_id=config['mqtt']['client_id'],
+ self._client = mqtt.Client(client_id=client_id,
protocol=mqtt.MQTTv311,
clean_session=clean_session)
self._client.on_connect = self.on_connect
@@ -24,30 +29,34 @@ class MqttBase:
self._client.on_log = self.on_log
self._client.on_publish = self.on_publish
self._loop_started = False
-
+ self._connected = False
+ self._is_server = is_server
+ self._mqtt_config = MqttConfig()
self._logger = logging.getLogger(self.__class__.__name__)
- username, password = username_and_password()
- if username and password:
- self._logger.debug(f'username={username} password={password}')
- self._client.username_pw_set(username, password)
+ if not creds:
+ creds = self._mqtt_config.creds() if not is_server else self._mqtt_config.server_creds()
+
+ 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, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
+ self._client.tls_set(ca_certs=ca_certs,
+ cert_reqs=ssl.CERT_REQUIRED,
+ tls_version=ssl.PROTOCOL_TLSv1_2)
def connect_and_loop(self, loop_forever=True):
- host = config['mqtt']['host']
- port = config['mqtt']['port']
-
- self._client.connect(host, port, 60)
+ 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:
self._client.loop_forever()
else:
@@ -61,9 +70,11 @@ class MqttBase:
def on_connect(self, client: mqtt.Client, userdata, flags, rc):
self._logger.info("Connected with result code " + str(rc))
+ self._connected = True
def on_disconnect(self, client: mqtt.Client, userdata, rc):
self._logger.info("Disconnected with result code " + str(rc))
+ self._connected = False
def on_log(self, client: mqtt.Client, userdata, level, buf):
level = mqtt.LOGGING_LEVEL[level] if level in mqtt.LOGGING_LEVEL else logging.INFO
@@ -73,4 +84,4 @@ class MqttBase:
self._logger.debug(msg.topic + ": " + str(msg.payload))
def on_publish(self, client: mqtt.Client, userdata, mid):
- self._logger.debug(f'publish done, mid={mid}') \ No newline at end of file
+ self._logger.debug(f'publish done, mid={mid}')
diff --git a/include/py/homekit/mqtt/_node.py b/include/py/homekit/mqtt/_node.py
new file mode 100644
index 0000000..4e259a4
--- /dev/null
+++ b/include/py/homekit/mqtt/_node.py
@@ -0,0 +1,92 @@
+import logging
+import importlib
+
+from typing import List, TYPE_CHECKING, Optional
+from ._payload import MqttPayload
+from ._module import MqttModule
+if TYPE_CHECKING:
+ from ._wrapper import MqttWrapper
+else:
+ MqttWrapper = None
+
+
+class MqttNode:
+ _modules: List[MqttModule]
+ _module_subscriptions: dict[str, MqttModule]
+ _node_id: str
+ _node_secret: str
+ _payload_callbacks: list[callable]
+ _wrapper: Optional[MqttWrapper]
+
+ def __init__(self,
+ node_id: str,
+ node_secret: Optional[str] = None):
+ self._modules = []
+ self._module_subscriptions = {}
+ self._node_id = node_id
+ self._node_secret = node_secret
+ self._payload_callbacks = []
+ self._logger = logging.getLogger(self.__class__.__name__)
+ self._wrapper = None
+
+ def on_connect(self, wrapper: MqttWrapper):
+ self._wrapper = wrapper
+ for module in self._modules:
+ if not module.is_initialized():
+ module.on_connect(self)
+ module.set_initialized()
+
+ def on_disconnect(self):
+ self._wrapper = None
+ for module in self._modules:
+ module.unset_initialized()
+
+ def on_message(self, topic, payload):
+ if topic in self._module_subscriptions:
+ payload = self._module_subscriptions[topic].handle_payload(self, topic, payload)
+ if isinstance(payload, MqttPayload):
+ for f in self._payload_callbacks:
+ f(self, payload)
+
+ def load_module(self, module_name: str, *args, **kwargs) -> MqttModule:
+ module = importlib.import_module(f'..module.{module_name}', __name__)
+ if not hasattr(module, 'MODULE_NAME'):
+ raise RuntimeError(f'MODULE_NAME not found in module {module}')
+ cl = getattr(module, getattr(module, 'MODULE_NAME'))
+ instance = cl(*args, **kwargs)
+ self.add_module(instance)
+ return instance
+
+ def add_module(self, module: MqttModule):
+ self._modules.append(module)
+ if self._wrapper and self._wrapper._connected:
+ module.on_connect(self)
+ module.set_initialized()
+
+ def subscribe_module(self, topic: str, module: MqttModule, qos: int = 1):
+ if not self._wrapper or not self._wrapper._connected:
+ raise RuntimeError('not connected')
+
+ self._module_subscriptions[topic] = module
+ self._wrapper.subscribe(self.id, topic, qos)
+
+ def publish(self,
+ topic: str,
+ payload: bytes,
+ qos: int = 1):
+ self._wrapper.publish(self.id, topic, payload, qos)
+
+ def add_payload_callback(self, callback: callable):
+ self._payload_callbacks.append(callback)
+
+ @property
+ def id(self) -> str:
+ return self._node_id
+
+ @property
+ def secret(self) -> str:
+ return self._node_secret
+
+ @secret.setter
+ def secret(self, secret: str) -> None:
+ self._node_secret = secret
diff --git a/src/home/mqtt/payload/base_payload.py b/include/py/homekit/mqtt/_payload.py
index 1abd898..58eeae3 100644
--- a/src/home/mqtt/payload/base_payload.py
+++ b/include/py/homekit/mqtt/_payload.py
@@ -1,5 +1,5 @@
-import abc
import struct
+import abc
import re
from typing import Optional, Tuple
@@ -142,4 +142,4 @@ def _bit_field_params(cl) -> Optional[Tuple[int, ...]]:
match = re.match(r'MQTTPayloadBitField_(\d+)_(\d+)_(\d)$', cl.__name__)
if match is not None:
return tuple([int(match.group(i)) for i in range(1, 4)])
- return None
+ return None \ No newline at end of file
diff --git a/include/py/homekit/mqtt/_util.py b/include/py/homekit/mqtt/_util.py
new file mode 100644
index 0000000..390d463
--- /dev/null
+++ b/include/py/homekit/mqtt/_util.py
@@ -0,0 +1,15 @@
+import os
+import re
+
+from typing import List
+
+
+def get_modules() -> List[str]:
+ modules = []
+ modules_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'module')
+ for name in os.listdir(modules_dir):
+ if os.path.isdir(os.path.join(modules_dir, name)):
+ continue
+ name = re.sub(r'\.py$', '', name)
+ modules.append(name)
+ return modules
diff --git a/include/py/homekit/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py
new file mode 100644
index 0000000..3c2774c
--- /dev/null
+++ b/include/py/homekit/mqtt/_wrapper.py
@@ -0,0 +1,60 @@
+import paho.mqtt.client as mqtt
+
+from ._mqtt import Mqtt
+from ._node import MqttNode
+from ..util import strgen
+
+
+class MqttWrapper(Mqtt):
+ _nodes: list[MqttNode]
+
+ def __init__(self,
+ client_id: str,
+ topic_prefix='hk',
+ randomize_client_id=False,
+ clean_session=True):
+ if randomize_client_id:
+ client_id += '_'+strgen(6)
+ super().__init__(clean_session=clean_session,
+ client_id=client_id)
+ self._nodes = []
+ 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)
+
+ def on_disconnect(self, client: mqtt.Client, userdata, rc):
+ super().on_disconnect(client, userdata, rc)
+ for node in self._nodes:
+ node.on_disconnect()
+
+ 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:
+ 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_node(self, node: MqttNode):
+ self._nodes.append(node)
+ if self._connected:
+ node.on_connect(self)
+
+ def subscribe(self,
+ node_id: str,
+ topic: str,
+ qos: int):
+ self._client.subscribe(f'{self._topic_prefix}/{node_id}/{topic}', qos)
+
+ def publish(self,
+ node_id: str,
+ topic: str,
+ payload: bytes,
+ qos: int):
+ self._client.publish(f'{self._topic_prefix}/{node_id}/{topic}', payload, qos)
+ self._client.loop_write()
diff --git a/src/home/mqtt/payload/esp.py b/include/py/homekit/mqtt/module/diagnostics.py
index 171cdb9..5db5e99 100644
--- a/src/home/mqtt/payload/esp.py
+++ b/include/py/homekit/mqtt/module/diagnostics.py
@@ -1,39 +1,8 @@
-import hashlib
+from .._payload import MqttPayload, MqttPayloadCustomField
+from .._node import MqttNode, MqttModule
+from typing import Optional
-from .base_payload import MqttPayload, MqttPayloadCustomField
-
-
-class OTAResultPayload(MqttPayload):
- FORMAT = '=BB'
- result: int
- error_code: int
-
-
-class OTAPayload(MqttPayload):
- secret: str
- filename: str
-
- # structure of returned data:
- #
- # uint8_t[len(secret)] secret;
- # uint8_t[16] md5;
- # *uint8_t data
-
- def pack(self):
- buf = bytearray(self.secret.encode())
- m = hashlib.md5()
- with open(self.filename, 'rb') as fd:
- content = fd.read()
- m.update(content)
- buf.extend(m.digest())
- buf.extend(content)
- return buf
-
- def unpack(cls, buf: bytes):
- raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented')
- # secret = buf[:12].decode()
- # filename = buf[12:].decode()
- # return OTAPayload(secret=secret, filename=filename)
+MODULE_NAME = 'MqttDiagnosticsModule'
class DiagnosticsFlags(MqttPayloadCustomField):
@@ -76,3 +45,20 @@ class DiagnosticsPayload(MqttPayload):
rssi: int
free_heap: int
flags: DiagnosticsFlags
+
+
+class MqttDiagnosticsModule(MqttModule):
+ def on_connect(self, mqtt: MqttNode):
+ super().on_connect(mqtt)
+ for topic in ('diag', 'd1ag', 'stat', 'stat1'):
+ mqtt.subscribe_module(topic, self)
+
+ def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]:
+ message = None
+ if topic in ('stat', 'diag'):
+ message = DiagnosticsPayload.unpack(payload)
+ elif topic in ('stat1', 'd1ag'):
+ message = InitialDiagnosticsPayload.unpack(payload)
+ if message:
+ self._logger.debug(message)
+ return message
diff --git a/include/py/homekit/mqtt/module/inverter.py b/include/py/homekit/mqtt/module/inverter.py
new file mode 100644
index 0000000..29bde0a
--- /dev/null
+++ b/include/py/homekit/mqtt/module/inverter.py
@@ -0,0 +1,195 @@
+import time
+import json
+import datetime
+try:
+ import inverterd
+except:
+ pass
+
+from typing import Optional
+from .._module import MqttModule
+from .._node import MqttNode
+from .._payload import MqttPayload, bit_field
+try:
+ from homekit.database import InverterDatabase
+except:
+ pass
+
+_mult_10 = lambda n: int(n*10)
+_div_10 = lambda n: n/10
+
+
+MODULE_NAME = 'MqttInverterModule'
+
+STATUS_TOPIC = 'status'
+GENERATION_TOPIC = 'generation'
+
+
+class MqttInverterStatusPayload(MqttPayload):
+ # 46 bytes
+ FORMAT = 'IHHHHHHBHHHHHBHHHHHHHH'
+
+ PACKER = {
+ 'grid_voltage': _mult_10,
+ 'grid_freq': _mult_10,
+ 'ac_output_voltage': _mult_10,
+ 'ac_output_freq': _mult_10,
+ 'battery_voltage': _mult_10,
+ 'battery_voltage_scc': _mult_10,
+ 'battery_voltage_scc2': _mult_10,
+ 'pv1_input_voltage': _mult_10,
+ 'pv2_input_voltage': _mult_10
+ }
+ UNPACKER = {
+ 'grid_voltage': _div_10,
+ 'grid_freq': _div_10,
+ 'ac_output_voltage': _div_10,
+ 'ac_output_freq': _div_10,
+ 'battery_voltage': _div_10,
+ 'battery_voltage_scc': _div_10,
+ 'battery_voltage_scc2': _div_10,
+ 'pv1_input_voltage': _div_10,
+ 'pv2_input_voltage': _div_10
+ }
+
+ time: int
+ grid_voltage: float
+ grid_freq: float
+ ac_output_voltage: float
+ ac_output_freq: float
+ ac_output_apparent_power: int
+ ac_output_active_power: int
+ output_load_percent: int
+ battery_voltage: float
+ battery_voltage_scc: float
+ battery_voltage_scc2: float
+ battery_discharge_current: int
+ battery_charge_current: int
+ battery_capacity: int
+ inverter_heat_sink_temp: int
+ mppt1_charger_temp: int
+ mppt2_charger_temp: int
+ pv1_input_power: int
+ pv2_input_power: int
+ pv1_input_voltage: float
+ pv2_input_voltage: float
+
+ # H
+ mppt1_charger_status: bit_field(0, 16, 2)
+ mppt2_charger_status: bit_field(0, 16, 2)
+ battery_power_direction: bit_field(0, 16, 2)
+ dc_ac_power_direction: bit_field(0, 16, 2)
+ line_power_direction: bit_field(0, 16, 2)
+ load_connected: bit_field(0, 16, 1)
+
+
+class MqttInverterGenerationPayload(MqttPayload):
+ # 8 bytes
+ FORMAT = 'II'
+
+ time: int
+ wh: int
+
+
+class MqttInverterModule(MqttModule):
+ _status_poll_freq: int
+ _generation_poll_freq: int
+ _inverter: Optional[inverterd.Client]
+ _database: Optional[InverterDatabase]
+ _gen_prev: float
+
+ def __init__(self, status_poll_freq=0, generation_poll_freq=0):
+ super().__init__(tick_interval=status_poll_freq)
+ self._status_poll_freq = status_poll_freq
+ self._generation_poll_freq = generation_poll_freq
+
+ # this defines whether this is a publisher or a subscriber
+ if status_poll_freq > 0:
+ self._inverter = inverterd.Client()
+ self._inverter.connect()
+ self._inverter.format(inverterd.Format.SIMPLE_JSON)
+ self._database = None
+ else:
+ self._inverter = None
+ self._database = InverterDatabase()
+
+ self._gen_prev = 0
+
+ def on_connect(self, mqtt: MqttNode):
+ super().on_connect(mqtt)
+ if not self._inverter:
+ mqtt.subscribe_module(STATUS_TOPIC, self)
+ mqtt.subscribe_module(GENERATION_TOPIC, self)
+
+ def tick(self):
+ if not self._inverter:
+ return
+
+ # read status
+ now = time.time()
+ try:
+ raw = self._inverter.exec('get-status')
+ except inverterd.InverterError as e:
+ self._logger.error(f'inverter error: {str(e)}')
+ # TODO send to server
+ return
+
+ data = json.loads(raw)['data']
+ status = MqttInverterStatusPayload(time=round(now), **data)
+ self._mqtt_node_ref.publish(STATUS_TOPIC, status.pack())
+
+ # read today's generation stat
+ now = time.time()
+ if self._gen_prev == 0 or now - self._gen_prev >= self._generation_poll_freq:
+ self._gen_prev = now
+ today = datetime.date.today()
+ try:
+ raw = self._inverter.exec('get-day-generated', (today.year, today.month, today.day))
+ except inverterd.InverterError as e:
+ self._logger.error(f'inverter error: {str(e)}')
+ # TODO send to server
+ return
+
+ data = json.loads(raw)['data']
+ gen = MqttInverterGenerationPayload(time=round(now), wh=data['wh'])
+ self._mqtt_node_ref.publish(GENERATION_TOPIC, gen.pack())
+
+ def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]:
+ home_id = 1 # legacy compat
+
+ if topic == STATUS_TOPIC:
+ s = MqttInverterStatusPayload.unpack(payload)
+ self._database.add_status(home_id=home_id,
+ client_time=s.time,
+ grid_voltage=int(s.grid_voltage*10),
+ grid_freq=int(s.grid_freq * 10),
+ ac_output_voltage=int(s.ac_output_voltage * 10),
+ ac_output_freq=int(s.ac_output_freq * 10),
+ ac_output_apparent_power=s.ac_output_apparent_power,
+ ac_output_active_power=s.ac_output_active_power,
+ output_load_percent=s.output_load_percent,
+ battery_voltage=int(s.battery_voltage * 10),
+ battery_voltage_scc=int(s.battery_voltage_scc * 10),
+ battery_voltage_scc2=int(s.battery_voltage_scc2 * 10),
+ battery_discharge_current=s.battery_discharge_current,
+ battery_charge_current=s.battery_charge_current,
+ battery_capacity=s.battery_capacity,
+ inverter_heat_sink_temp=s.inverter_heat_sink_temp,
+ mppt1_charger_temp=s.mppt1_charger_temp,
+ mppt2_charger_temp=s.mppt2_charger_temp,
+ pv1_input_power=s.pv1_input_power,
+ pv2_input_power=s.pv2_input_power,
+ pv1_input_voltage=int(s.pv1_input_voltage * 10),
+ pv2_input_voltage=int(s.pv2_input_voltage * 10),
+ mppt1_charger_status=s.mppt1_charger_status,
+ mppt2_charger_status=s.mppt2_charger_status,
+ battery_power_direction=s.battery_power_direction,
+ dc_ac_power_direction=s.dc_ac_power_direction,
+ line_power_direction=s.line_power_direction,
+ load_connected=s.load_connected)
+ return s
+
+ elif topic == GENERATION_TOPIC:
+ gen = MqttInverterGenerationPayload.unpack(payload)
+ self._database.add_generation(home_id, gen.time, gen.wh)
+ return gen
diff --git a/include/py/homekit/mqtt/module/ota.py b/include/py/homekit/mqtt/module/ota.py
new file mode 100644
index 0000000..cd34332
--- /dev/null
+++ b/include/py/homekit/mqtt/module/ota.py
@@ -0,0 +1,77 @@
+import hashlib
+
+from typing import Optional
+from .._payload import MqttPayload
+from .._node import MqttModule, MqttNode
+
+MODULE_NAME = 'MqttOtaModule'
+
+
+class OtaResultPayload(MqttPayload):
+ FORMAT = '=BB'
+ result: int
+ error_code: int
+
+
+class OtaPayload(MqttPayload):
+ secret: str
+ filename: str
+
+ # structure of returned data:
+ #
+ # uint8_t[len(secret)] secret;
+ # uint8_t[16] md5;
+ # *uint8_t data
+
+ def pack(self):
+ buf = bytearray(self.secret.encode())
+ m = hashlib.md5()
+ with open(self.filename, 'rb') as fd:
+ content = fd.read()
+ m.update(content)
+ buf.extend(m.digest())
+ buf.extend(content)
+ return buf
+
+ def unpack(cls, buf: bytes):
+ raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented')
+ # secret = buf[:12].decode()
+ # filename = buf[12:].decode()
+ # return OTAPayload(secret=secret, filename=filename)
+
+
+class MqttOtaModule(MqttModule):
+ _ota_request: Optional[tuple[str, int]]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._ota_request = None
+
+ def on_connect(self, mqtt: MqttNode):
+ super().on_connect(mqtt)
+ mqtt.subscribe_module("otares", self)
+
+ if self._ota_request is not None:
+ filename, qos = self._ota_request
+ self._ota_request = None
+ self.do_push_ota(self._mqtt_node_ref.secret, filename, qos)
+
+ def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]:
+ if topic == 'otares':
+ message = OtaResultPayload.unpack(payload)
+ self._logger.debug(message)
+ return message
+
+ def do_push_ota(self, secret: str, filename: str, qos: int):
+ payload = OtaPayload(secret=secret, filename=filename)
+ self._mqtt_node_ref.publish('ota',
+ payload=payload.pack(),
+ qos=qos)
+
+ def push_ota(self,
+ filename: str,
+ qos: int):
+ if not self._initialized:
+ self._ota_request = (filename, qos)
+ else:
+ self.do_push_ota(filename, qos)
diff --git a/include/py/homekit/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py
new file mode 100644
index 0000000..e968031
--- /dev/null
+++ b/include/py/homekit/mqtt/module/relay.py
@@ -0,0 +1,92 @@
+import datetime
+
+from typing import Optional
+from .. import MqttModule, MqttPayload, MqttNode
+
+MODULE_NAME = 'MqttRelayModule'
+
+
+class MqttPowerSwitchPayload(MqttPayload):
+ FORMAT = '=12sB'
+ PACKER = {
+ 'state': lambda n: int(n),
+ 'secret': lambda s: s.encode('utf-8')
+ }
+ UNPACKER = {
+ 'state': lambda n: bool(n),
+ 'secret': lambda s: s.decode('utf-8')
+ }
+
+ secret: str
+ state: bool
+
+
+class MqttPowerStatusPayload(MqttPayload):
+ FORMAT = '=B'
+ PACKER = {
+ 'opened': lambda n: int(n),
+ }
+ UNPACKER = {
+ 'opened': lambda n: bool(n),
+ }
+
+ opened: bool
+
+
+class MqttRelayState:
+ enabled: bool
+ update_time: datetime.datetime
+ rssi: int
+ fw_version: int
+ ever_updated: bool
+
+ def __init__(self):
+ self.ever_updated = False
+ self.enabled = False
+ self.rssi = 0
+
+ def update(self,
+ enabled: bool,
+ rssi: int,
+ fw_version=None):
+ self.ever_updated = True
+ self.enabled = enabled
+ self.rssi = rssi
+ self.update_time = datetime.datetime.now()
+ if fw_version:
+ self.fw_version = fw_version
+
+
+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(self._get_switch_topic(), self)
+ mqtt.subscribe_module('relay/status', self)
+
+ def switchpower(self,
+ enable: bool):
+ payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret,
+ state=enable)
+ 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 == self._get_switch_topic():
+ message = MqttPowerSwitchPayload.unpack(payload)
+ elif topic == 'relay/status':
+ message = MqttPowerStatusPayload.unpack(payload)
+
+ 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/include/py/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py
new file mode 100644
index 0000000..fd02cca
--- /dev/null
+++ b/include/py/homekit/mqtt/module/temphum.py
@@ -0,0 +1,82 @@
+from .._node import MqttNode
+from .._module import MqttModule
+from .._payload import MqttPayload
+from typing import Optional
+from ...temphum import BaseSensor
+
+two_digits_precision = lambda x: round(x, 2)
+
+MODULE_NAME = 'MqttTempHumModule'
+DATA_TOPIC = 'temphum/data'
+
+
+class MqttTemphumDataPayload(MqttPayload):
+ FORMAT = '=ddb'
+ UNPACKER = {
+ 'temp': two_digits_precision,
+ 'rh': two_digits_precision
+ }
+
+ 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 MqttTempHumModule(MqttModule):
+ def __init__(self,
+ sensor: Optional[BaseSensor] = None,
+ write_to_database=False,
+ *args, **kwargs):
+ if sensor is not None:
+ kwargs['tick_interval'] = 10
+ super().__init__(*args, **kwargs)
+ self._sensor = sensor
+
+ def on_connect(self, mqtt: MqttNode):
+ super().on_connect(mqtt)
+ mqtt.subscribe_module(DATA_TOPIC, self)
+
+ def tick(self):
+ if not self._sensor:
+ return
+
+ error = 0
+ temp = 0
+ rh = 0
+ try:
+ temp = self._sensor.temperature()
+ rh = self._sensor.humidity()
+ except:
+ error = 1
+ pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error)
+ self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack())
+
+ def handle_payload(self,
+ mqtt: MqttNode,
+ topic: str,
+ payload: bytes) -> Optional[MqttPayload]:
+ if topic == DATA_TOPIC:
+ message = MqttTemphumDataPayload.unpack(payload)
+ self._logger.debug(message)
+ return message
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 7649078..a0e7a1f 100644
--- a/src/home/pio/products.py
+++ b/include/py/homekit/pio/products.py
@@ -8,18 +8,14 @@ from collections import OrderedDict
_logger = logging.getLogger(__name__)
_products_dir = os.path.join(
os.path.dirname(__file__),
- '..', '..', '..',
- 'platformio'
+ '..', '..', '..', '..',
+ 'pio'
)
def get_products():
products = []
for f in os.listdir(_products_dir):
- # temp hack
- if f.endswith('-esp01'):
- continue
- # skip the common dir
if f in ('common',):
continue
@@ -93,8 +89,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:
@@ -111,7 +109,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/include/py/homekit/telegram/config.py b/include/py/homekit/telegram/config.py
new file mode 100644
index 0000000..5f41008
--- /dev/null
+++ b/include/py/homekit/telegram/config.py
@@ -0,0 +1,78 @@
+from ..config import ConfigUnit
+from typing import Optional, Union
+from abc import ABC
+from enum import Enum
+
+
+class TelegramUserListType(Enum):
+ USERS = 'users'
+ NOTIFY = 'notify_users'
+
+
+class TelegramUserIdsConfig(ConfigUnit):
+ NAME = 'telegram_user_ids'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'roottype': 'dict',
+ 'type': 'integer'
+ }
+
+
+_user_ids_config = TelegramUserIdsConfig()
+
+
+def _user_id_mapper(user: Union[str, int]) -> int:
+ if isinstance(user, int):
+ return user
+ return _user_ids_config[user]
+
+
+class TelegramChatsConfig(ConfigUnit):
+ NAME = 'telegram_chats'
+
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'type': 'dict',
+ 'schema': {
+ 'id': {'type': 'string', 'required': True},
+ 'token': {'type': 'string', 'required': True},
+ }
+ }
+
+
+class TelegramBotConfig(ConfigUnit, ABC):
+ @classmethod
+ def schema(cls) -> Optional[dict]:
+ return {
+ 'bot': {
+ 'type': 'dict',
+ 'schema': {
+ 'token': {'type': 'string', 'required': True},
+ 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', 'integer']}}
+
+ @staticmethod
+ def custom_validator(data):
+ for ult in TelegramUserListType:
+ users = data['bot'][ult.value]
+ for user in users:
+ if isinstance(user, str):
+ if user not in _user_ids_config:
+ raise ValueError(f'user {user} not found in {TelegramUserIdsConfig.NAME}')
+
+ def get_user_ids(self,
+ ult: TelegramUserListType = TelegramUserListType.USERS) -> list[int]:
+ try:
+ return list(map(_user_id_mapper, self['bot'][ult.value]))
+ except KeyError:
+ return [] \ No newline at end of file
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/include/py/homekit/temphum/__init__.py b/include/py/homekit/temphum/__init__.py
new file mode 100644
index 0000000..46d14e6
--- /dev/null
+++ b/include/py/homekit/temphum/__init__.py
@@ -0,0 +1 @@
+from .base import SensorType, BaseSensor
diff --git a/src/home/temphum/base.py b/include/py/homekit/temphum/base.py
index e774433..602cab7 100644
--- a/src/home/temphum/base.py
+++ b/include/py/homekit/temphum/base.py
@@ -1,25 +1,19 @@
-import smbus
-
-from abc import abstractmethod, ABC
+from abc import ABC
from enum import Enum
-class TempHumSensor:
- @abstractmethod
+class BaseSensor(ABC):
+ def __init__(self, bus: int):
+ super().__init__()
+ self.bus = smbus.SMBus(bus)
+
def humidity(self) -> float:
pass
- @abstractmethod
def temperature(self) -> float:
pass
-class I2CTempHumSensor(TempHumSensor, ABC):
- def __init__(self, bus: int):
- super().__init__()
- self.bus = smbus.SMBus(bus)
-
-
class SensorType(Enum):
Si7021 = 'si7021'
- DHT12 = 'dht12'
+ DHT12 = 'dht12' \ No newline at end of file
diff --git a/include/py/homekit/temphum/i2c.py b/include/py/homekit/temphum/i2c.py
new file mode 100644
index 0000000..7d8e2e3
--- /dev/null
+++ b/include/py/homekit/temphum/i2c.py
@@ -0,0 +1,52 @@
+import abc
+import smbus
+
+from .base import BaseSensor, SensorType
+
+
+class I2CSensor(BaseSensor, abc.ABC):
+ def __init__(self, bus: int):
+ super().__init__()
+ self.bus = smbus.SMBus(bus)
+
+
+class DHT12(I2CSensor):
+ i2c_addr = 0x5C
+
+ def _measure(self):
+ raw = self.bus.read_i2c_block_data(self.i2c_addr, 0, 5)
+ if (raw[0] + raw[1] + raw[2] + raw[3]) & 0xff != raw[4]:
+ raise ValueError("checksum error")
+ return raw
+
+ def temperature(self) -> float:
+ raw = self._measure()
+ temp = raw[2] + (raw[3] & 0x7f) * 0.1
+ if raw[3] & 0x80:
+ temp *= -1
+ return temp
+
+ def humidity(self) -> float:
+ raw = self._measure()
+ return raw[0] + raw[1] * 0.1
+
+
+class Si7021(I2CSensor):
+ i2c_addr = 0x40
+
+ def temperature(self) -> float:
+ raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE3, 2)
+ return 175.72 * (raw[0] << 8 | raw[1]) / 65536.0 - 46.85
+
+ def humidity(self) -> float:
+ raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE5, 2)
+ return 125.0 * (raw[0] << 8 | raw[1]) / 65536.0 - 6.0
+
+
+def create_sensor(type: SensorType, bus: int) -> BaseSensor:
+ if type == SensorType.Si7021:
+ return Si7021(bus)
+ elif type == SensorType.DHT12:
+ return DHT12(bus)
+ else:
+ raise ValueError('unexpected sensor type')
diff --git a/src/home/util.py b/include/py/homekit/util.py
index 93a9d8f..22bba86 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,17 +8,82 @@ import traceback
import logging
import string
import random
+import re
from enum import Enum
from datetime import datetime
-from typing import Tuple, Optional, List
+from typing import Optional, List
from zlib import adler32
-Addr = Tuple[str, int] # network address type (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_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
+
+ @staticmethod
+ def fromstring(addr: str) -> Addr:
+ colons = addr.count(':')
+ if colons != 1:
+ raise ValueError('invalid host:port format')
+
+ if not colons:
+ host = addr
+ port = None
+ else:
+ host, port = addr.split(':')
+
+ 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)
+
+ def __str__(self):
+ buf = self.host
+ if self.port is not None:
+ buf += ':'+str(self.port)
+ return buf
+
+ 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."""
@@ -45,21 +112,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 host, port
-
-
def strgen(n: int):
return ''.join(random.choices(string.ascii_letters + string.digits, k=n))
@@ -193,4 +245,11 @@ def filesize_fmt(num, suffix="B") -> str:
class HashableEnum(Enum):
def hash(self) -> int:
- return adler32(self.name.encode()) \ No newline at end of file
+ return adler32(self.name.encode())
+
+
+def next_tick_gen(freq):
+ t = time.time()
+ while True:
+ t += freq
+ yield max(t - time.time(), 0) \ No newline at end of file
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/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/misc/openwrt/etc/rc.local b/misc/openwrt/etc/rc.local
index 407d1eb..32b1227 100644
--- a/misc/openwrt/etc/rc.local
+++ b/misc/openwrt/etc/rc.local
@@ -17,7 +17,7 @@ done
sleep 0.1
# block internet access for untrusted cameras
-iptables -I FORWARD 1 -m set --match-set ipcam src ! -d 192.168.5.0 -j REJECT
+iptables -I FORWARD 1 -m set --match-set ipcam src ! -d 192.168.5.0/24 -j REJECT
# add some default routing rules
ipset add mts-azov 192.168.5.0/24 # everybody
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 0b05316..7f0945e 100644
--- a/platformio/temphum_relayctl/src/main.cpp
+++ b/pio/temphum_relayctl/src/main.cpp
@@ -27,6 +27,7 @@ void setup() {
main::setup();
relay::init();
+ relay::off();
#if CONFIG_MODULE == HOMEKIT_SI7021
sensor = new temphum::Si7021();
diff --git a/platformio/common/libs/main/library.json b/platformio/common/libs/main/library.json
deleted file mode 100644
index 04eedab..0000000
--- a/platformio/common/libs/main/library.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "homekit_main",
- "version": "1.0.8",
- "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 30db7d2..0000000
--- a/platformio/common/libs/mqtt_module_ota/library.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "homekit_mqtt_module_ota",
- "version": "1.0.2",
- "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 e71cf95..0000000
--- a/platformio/common/libs/mqtt_module_relay/library.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "homekit_mqtt_module_relay",
- "version": "1.0.3",
- "build": {
- "flags": "-I../../include"
- },
- "dependencies": {
- "homekit_mqtt": "file://../common/libs/mqtt",
- "homekit_relay": "file://../common/libs/relay"
- }
-}
diff --git a/platformio/common/libs/mqtt_module_temphum/library.json b/platformio/common/libs/mqtt_module_temphum/library.json
deleted file mode 100644
index 9bb8cf1..0000000
--- a/platformio/common/libs/mqtt_module_temphum/library.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "homekit_mqtt_module_temphum",
- "version": "1.0.9",
- "build": {
- "flags": "-I../../include"
- },
- "dependencies": {
- "homekit_mqtt": "file://../common/libs/mqtt",
- "homekit_temphum": "file://../common/libs/temphum"
- }
-}
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 46f9b8c..521ae41 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,25 +1,20 @@
paho-mqtt==1.6.1
inverterd~=1.0.3
clickhouse-driver~=0.2.0
-toml~=0.10.2
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
+python-telegram-bot==20.3
+requests==2.31.0
aiohttp~=3.8.1
-pytz==2022.6
+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
# following can be installed from debian repositories
# matplotlib~=3.5.0
-Pillow~=9.1.1
-
-# for polaris kettle protocol implementation
-cryptography==38.0.4
-zeroconf==0.39.4 \ No newline at end of file
+Pillow==9.5.0 \ No newline at end of file
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/esp_mqtt_util.py b/src/esp_mqtt_util.py
deleted file mode 100755
index 263128c..0000000
--- a/src/esp_mqtt_util.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-from typing import Optional
-from argparse import ArgumentParser
-from enum import Enum
-
-from home.config import config
-from home.mqtt import MqttRelay
-from home.mqtt.esp import MqttEspBase
-from home.mqtt.temphum import MqttTempHum
-from home.mqtt.esp import MqttEspDevice
-
-mqtt_client: Optional[MqttEspBase] = None
-
-
-class NodeType(Enum):
- RELAY = 'relay'
- TEMPHUM = 'temphum'
-
-
-if __name__ == '__main__':
- parser = ArgumentParser()
- parser.add_argument('--device-id', type=str, required=True)
- parser.add_argument('--type', type=str, required=True,
- choices=[i.name.lower() for i in NodeType])
-
- config.load('mqtt_util', parser=parser)
- arg = parser.parse_args()
-
- mqtt_node_type = NodeType(arg.type)
- devices = MqttEspDevice(id=arg.device_id)
-
- if mqtt_node_type == NodeType.RELAY:
- mqtt_client = MqttRelay(devices=devices)
- elif mqtt_node_type == NodeType.TEMPHUM:
- mqtt_client = MqttTempHum(devices=devices)
-
- mqtt_client.set_message_callback(lambda device_id, payload: print(payload))
- mqtt_client.configure_tls()
- try:
- mqtt_client.connect_and_loop()
- except KeyboardInterrupt:
- mqtt_client.disconnect()
diff --git a/src/gpiorelayd.py b/src/gpiorelayd.py
deleted file mode 100755
index 85015a7..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()
-
- 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/config/__init__.py b/src/home/config/__init__.py
deleted file mode 100644
index cc9c091..0000000
--- a/src/home/config/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .config import ConfigStore, config, is_development_mode, setup_logging
diff --git a/src/home/config/config.py b/src/home/config/config.py
deleted file mode 100644
index 4681685..0000000
--- a/src/home/config/config.py
+++ /dev/null
@@ -1,204 +0,0 @@
-import toml
-import yaml
-import logging
-import os
-
-from os.path import join, isdir, isfile
-from typing import Optional, Any, MutableMapping
-from argparse import ArgumentParser
-from ..util import parse_addr
-
-
-def _get_config_path(name: str) -> str:
- formats = ['toml', 'yaml']
-
- dirname = join(os.environ['HOME'], '.config', name)
-
- if isdir(dirname):
- for fmt in formats:
- filename = join(dirname, f'config.{fmt}')
- if isfile(filename):
- return filename
-
- raise IOError(f'config not found in {dirname}')
-
- else:
- filenames = [join(os.environ['HOME'], '.config', f'{name}.{format}') for format in formats]
- for file in filenames:
- if isfile(file):
- return file
-
- raise IOError(f'config not found')
-
-
-class ConfigStore:
- data: MutableMapping[str, Any]
- app_name: Optional[str]
-
- def __init__(self):
- self.data = {}
- self.app_name = None
-
- def load(self, name: Optional[str] = None,
- use_cli=True,
- parser: ArgumentParser = None):
- self.app_name = name
-
- if (name is None) and (not use_cli):
- raise RuntimeError('either config name must be none or use_cli must be True')
-
- log_default_fmt = False
- log_file = None
- log_verbose = False
- no_config = name is False
-
- path = None
- if use_cli:
- if parser is None:
- parser = ArgumentParser()
- if not no_config:
- parser.add_argument('-c', '--config', type=str, required=name is None,
- help='Path to the config in TOML or YAML format')
- parser.add_argument('-V', '--verbose', action='store_true')
- parser.add_argument('--log-file', type=str)
- parser.add_argument('--log-default-fmt', action='store_true')
- args = parser.parse_args()
-
- if not no_config and args.config:
- path = args.config
-
- if args.verbose:
- log_verbose = True
- if args.log_file:
- log_file = args.log_file
- if args.log_default_fmt:
- log_default_fmt = args.log_default_fmt
-
- if not no_config and path is None:
- path = _get_config_path(name)
-
- if no_config:
- self.data = {}
- else:
- if path.endswith('.toml'):
- self.data = toml.load(path)
- elif path.endswith('.yaml'):
- with open(path, 'r') as fd:
- self.data = yaml.safe_load(fd)
-
- if 'logging' in self:
- if not log_file and 'file' in self['logging']:
- log_file = self['logging']['file']
- if log_default_fmt and 'default_fmt' in self['logging']:
- log_default_fmt = self['logging']['default_fmt']
-
- setup_logging(log_verbose, log_file, log_default_fmt)
-
- if use_cli:
- return args
-
- def __getitem__(self, key):
- return self.data[key]
-
- def __setitem__(self, key, value):
- raise NotImplementedError('overwriting config values is prohibited')
-
- def __contains__(self, key):
- return key in self.data
-
- def get(self, key: str, default=None):
- cur = self.data
- pts = key.split('.')
- for i in range(len(pts)):
- k = pts[i]
- if i < len(pts)-1:
- if k not in cur:
- raise KeyError(f'key {k} not found')
- else:
- return cur[k] if k in cur else default
- cur = self.data[k]
- raise KeyError(f'option {key} not found')
-
- def get_addr(self, key: str):
- return parse_addr(self.get(key))
-
- def items(self):
- return self.data.items()
-
-
-config = ConfigStore()
-
-
-def is_development_mode() -> bool:
- if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev':
- return True
-
- return ('logging' in config) and ('verbose' in config['logging']) and (config['logging']['verbose'] is True)
-
-
-def setup_logging(verbose=False, log_file=None, default_fmt=False):
- logging_level = logging.INFO
- if is_development_mode() or verbose:
- logging_level = logging.DEBUG
- _add_logging_level('TRACE', logging.DEBUG-5)
-
- log_config = {'level': logging_level}
- if not default_fmt:
- log_config['format'] = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-
- if log_file is not None:
- log_config['filename'] = log_file
- log_config['encoding'] = 'utf-8'
-
- logging.basicConfig(**log_config)
-
-
-# https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
-def _add_logging_level(levelName, levelNum, methodName=None):
- """
- Comprehensively adds a new logging level to the `logging` module and the
- currently configured logging class.
-
- `levelName` becomes an attribute of the `logging` module with the value
- `levelNum`. `methodName` becomes a convenience method for both `logging`
- itself and the class returned by `logging.getLoggerClass()` (usually just
- `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
- used.
-
- To avoid accidental clobberings of existing attributes, this method will
- raise an `AttributeError` if the level name is already an attribute of the
- `logging` module or if the method name is already present
-
- Example
- -------
- >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
- >>> logging.getLogger(__name__).setLevel("TRACE")
- >>> logging.getLogger(__name__).trace('that worked')
- >>> logging.trace('so did this')
- >>> logging.TRACE
- 5
-
- """
- if not methodName:
- methodName = levelName.lower()
-
- if hasattr(logging, levelName):
- raise AttributeError('{} already defined in logging module'.format(levelName))
- if hasattr(logging, methodName):
- raise AttributeError('{} already defined in logging module'.format(methodName))
- if hasattr(logging.getLoggerClass(), methodName):
- raise AttributeError('{} already defined in logger class'.format(methodName))
-
- # This method was inspired by the answers to Stack Overflow post
- # http://stackoverflow.com/q/2183233/2988730, especially
- # http://stackoverflow.com/a/13638084/2988730
- def logForLevel(self, message, *args, **kwargs):
- if self.isEnabledFor(levelNum):
- self._log(levelNum, message, args, **kwargs)
- def logToRoot(message, *args, **kwargs):
- logging.log(levelNum, message, *args, **kwargs)
-
- logging.addLevelName(levelNum, levelName)
- setattr(logging, levelName, levelNum)
- setattr(logging.getLoggerClass(), methodName, logForLevel)
- setattr(logging, methodName, logToRoot) \ No newline at end of file
diff --git a/src/home/mqtt/__init__.py b/src/home/mqtt/__init__.py
deleted file mode 100644
index 982e2b6..0000000
--- a/src/home/mqtt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .mqtt import MqttBase
-from .util import poll_tick
-from .relay import MqttRelay, MqttRelayState
-from .temphum import MqttTempHum \ No newline at end of file
diff --git a/src/home/mqtt/esp.py b/src/home/mqtt/esp.py
deleted file mode 100644
index 56ced83..0000000
--- a/src/home/mqtt/esp.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import re
-import paho.mqtt.client as mqtt
-
-from .mqtt import MqttBase
-from typing import Optional, Union
-from .payload.esp import (
- OTAPayload,
- OTAResultPayload,
- DiagnosticsPayload,
- InitialDiagnosticsPayload
-)
-
-
-class MqttEspDevice:
- id: str
- secret: Optional[str]
-
- def __init__(self, id: str, secret: Optional[str] = None):
- self.id = id
- self.secret = secret
-
-
-class MqttEspBase(MqttBase):
- _devices: list[MqttEspDevice]
- _message_callback: Optional[callable]
- _ota_publish_callback: Optional[callable]
-
- TOPIC_LEAF = 'esp'
-
- def __init__(self,
- devices: Union[MqttEspDevice, list[MqttEspDevice]],
- subscribe_to_updates=True):
- super().__init__(clean_session=True)
- if not isinstance(devices, list):
- devices = [devices]
- self._devices = devices
- self._message_callback = None
- self._ota_publish_callback = None
- self._subscribe_to_updates = subscribe_to_updates
- self._ota_mid = None
-
- def on_connect(self, client: mqtt.Client, userdata, flags, rc):
- super().on_connect(client, userdata, flags, rc)
-
- if self._subscribe_to_updates:
- for device in self._devices:
- topic = f'hk/{device.id}/{self.TOPIC_LEAF}/#'
- self._logger.debug(f"subscribing to {topic}")
- client.subscribe(topic, qos=1)
-
- def on_publish(self, client: mqtt.Client, userdata, mid):
- if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback:
- self._ota_publish_callback()
-
- def set_message_callback(self, callback: callable):
- self._message_callback = callback
-
- def on_message(self, client: mqtt.Client, userdata, msg):
- try:
- match = re.match(self.get_mqtt_topics(), msg.topic)
- self._logger.debug(f'topic: {msg.topic}')
- if not match:
- return
-
- device_id = match.group(1)
- subtopic = match.group(2)
-
- # try:
- next(d for d in self._devices if d.id == device_id)
- # except StopIteration:h
- # return
-
- message = None
- if subtopic == 'stat':
- message = DiagnosticsPayload.unpack(msg.payload)
- elif subtopic == 'stat1':
- message = InitialDiagnosticsPayload.unpack(msg.payload)
- elif subtopic == 'otares':
- message = OTAResultPayload.unpack(msg.payload)
-
- if message and self._message_callback:
- self._message_callback(device_id, message)
- return True
-
- except Exception as e:
- self._logger.exception(str(e))
-
- def push_ota(self,
- device_id,
- filename: str,
- publish_callback: callable,
- qos: int):
- device = next(d for d in self._devices if d.id == device_id)
- assert device.secret is not None, 'device secret not specified'
-
- self._ota_publish_callback = publish_callback
- payload = OTAPayload(secret=device.secret, filename=filename)
- publish_result = self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/admin/ota',
- payload=payload.pack(),
- qos=qos)
- self._ota_mid = publish_result.mid
- self._client.loop_write()
-
- @classmethod
- def get_mqtt_topics(cls, additional_topics: Optional[list[str]] = None):
- return rf'^hk/(.*?)/{cls.TOPIC_LEAF}/(stat|stat1|otares'+('|'+('|'.join(additional_topics)) if additional_topics else '')+')$' \ No newline at end of file
diff --git a/src/home/mqtt/payload/__init__.py b/src/home/mqtt/payload/__init__.py
deleted file mode 100644
index eee6709..0000000
--- a/src/home/mqtt/payload/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .base_payload import MqttPayload \ No newline at end of file
diff --git a/src/home/mqtt/payload/inverter.py b/src/home/mqtt/payload/inverter.py
deleted file mode 100644
index 09388df..0000000
--- a/src/home/mqtt/payload/inverter.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import struct
-
-from .base_payload import MqttPayload, bit_field
-from typing import Tuple
-
-_mult_10 = lambda n: int(n*10)
-_div_10 = lambda n: n/10
-
-
-class Status(MqttPayload):
- # 46 bytes
- FORMAT = 'IHHHHHHBHHHHHBHHHHHHHH'
-
- PACKER = {
- 'grid_voltage': _mult_10,
- 'grid_freq': _mult_10,
- 'ac_output_voltage': _mult_10,
- 'ac_output_freq': _mult_10,
- 'battery_voltage': _mult_10,
- 'battery_voltage_scc': _mult_10,
- 'battery_voltage_scc2': _mult_10,
- 'pv1_input_voltage': _mult_10,
- 'pv2_input_voltage': _mult_10
- }
- UNPACKER = {
- 'grid_voltage': _div_10,
- 'grid_freq': _div_10,
- 'ac_output_voltage': _div_10,
- 'ac_output_freq': _div_10,
- 'battery_voltage': _div_10,
- 'battery_voltage_scc': _div_10,
- 'battery_voltage_scc2': _div_10,
- 'pv1_input_voltage': _div_10,
- 'pv2_input_voltage': _div_10
- }
-
- time: int
- grid_voltage: float
- grid_freq: float
- ac_output_voltage: float
- ac_output_freq: float
- ac_output_apparent_power: int
- ac_output_active_power: int
- output_load_percent: int
- battery_voltage: float
- battery_voltage_scc: float
- battery_voltage_scc2: float
- battery_discharge_current: int
- battery_charge_current: int
- battery_capacity: int
- inverter_heat_sink_temp: int
- mppt1_charger_temp: int
- mppt2_charger_temp: int
- pv1_input_power: int
- pv2_input_power: int
- pv1_input_voltage: float
- pv2_input_voltage: float
-
- # H
- mppt1_charger_status: bit_field(0, 16, 2)
- mppt2_charger_status: bit_field(0, 16, 2)
- battery_power_direction: bit_field(0, 16, 2)
- dc_ac_power_direction: bit_field(0, 16, 2)
- line_power_direction: bit_field(0, 16, 2)
- load_connected: bit_field(0, 16, 1)
-
-
-class Generation(MqttPayload):
- # 8 bytes
- FORMAT = 'II'
-
- time: int
- wh: int
diff --git a/src/home/mqtt/payload/relay.py b/src/home/mqtt/payload/relay.py
deleted file mode 100644
index 4902991..0000000
--- a/src/home/mqtt/payload/relay.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from .base_payload import MqttPayload
-from .esp import (
- OTAResultPayload,
- OTAPayload,
- InitialDiagnosticsPayload,
- DiagnosticsPayload
-)
-
-
-class PowerPayload(MqttPayload):
- FORMAT = '=12sB'
- PACKER = {
- 'state': lambda n: int(n),
- 'secret': lambda s: s.encode('utf-8')
- }
- UNPACKER = {
- 'state': lambda n: bool(n),
- 'secret': lambda s: s.decode('utf-8')
- }
-
- secret: str
- state: bool
diff --git a/src/home/mqtt/payload/sensors.py b/src/home/mqtt/payload/sensors.py
deleted file mode 100644
index f99b307..0000000
--- a/src/home/mqtt/payload/sensors.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from .base_payload import MqttPayload
-
-_mult_100 = lambda n: int(n*100)
-_div_100 = lambda n: n/100
-
-
-class Temperature(MqttPayload):
- FORMAT = 'IhH'
- PACKER = {
- 'temp': _mult_100,
- 'rh': _mult_100,
- }
- UNPACKER = {
- 'temp': _div_100,
- 'rh': _div_100,
- }
-
- time: int
- temp: float
- rh: float
diff --git a/src/home/mqtt/payload/temphum.py b/src/home/mqtt/payload/temphum.py
deleted file mode 100644
index c0b744e..0000000
--- a/src/home/mqtt/payload/temphum.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from .base_payload import MqttPayload
-
-two_digits_precision = lambda x: round(x, 2)
-
-
-class TempHumDataPayload(MqttPayload):
- FORMAT = '=ddb'
- UNPACKER = {
- 'temp': two_digits_precision,
- 'rh': two_digits_precision
- }
-
- temp: float
- rh: float
- error: int
diff --git a/src/home/mqtt/relay.py b/src/home/mqtt/relay.py
deleted file mode 100644
index a90f19c..0000000
--- a/src/home/mqtt/relay.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import paho.mqtt.client as mqtt
-import re
-import datetime
-
-from .payload.relay import (
- PowerPayload,
-)
-from .esp import MqttEspBase
-
-
-class MqttRelay(MqttEspBase):
- TOPIC_LEAF = 'relay'
-
- def set_power(self, device_id, enable: bool, secret=None):
- device = next(d for d in self._devices if d.id == device_id)
- secret = secret if secret else device.secret
-
- assert secret is not None, 'device secret not specified'
-
- payload = PowerPayload(secret=secret,
- state=enable)
- self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/power',
- payload=payload.pack(),
- qos=1)
- self._client.loop_write()
-
- def on_message(self, client: mqtt.Client, userdata, msg):
- if super().on_message(client, userdata, msg):
- return
-
- try:
- match = re.match(self.get_mqtt_topics(['power']), msg.topic)
- if not match:
- return
-
- device_id = match.group(1)
- subtopic = match.group(2)
-
- message = None
- if subtopic == 'power':
- message = PowerPayload.unpack(msg.payload)
-
- if message and self._message_callback:
- self._message_callback(device_id, message)
-
- except Exception as e:
- self._logger.exception(str(e))
-
-
-class MqttRelayState:
- enabled: bool
- update_time: datetime.datetime
- rssi: int
- fw_version: int
- ever_updated: bool
-
- def __init__(self):
- self.ever_updated = False
- self.enabled = False
- self.rssi = 0
-
- def update(self,
- enabled: bool,
- rssi: int,
- fw_version=None):
- self.ever_updated = True
- self.enabled = enabled
- self.rssi = rssi
- self.update_time = datetime.datetime.now()
- if fw_version:
- self.fw_version = fw_version
diff --git a/src/home/mqtt/temphum.py b/src/home/mqtt/temphum.py
deleted file mode 100644
index 44810ef..0000000
--- a/src/home/mqtt/temphum.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import paho.mqtt.client as mqtt
-import re
-
-from enum import auto
-from .payload.temphum import TempHumDataPayload
-from .esp import MqttEspBase
-from ..util import HashableEnum
-
-
-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 MqttTempHum(MqttEspBase):
- TOPIC_LEAF = 'temphum'
-
- def on_message(self, client: mqtt.Client, userdata, msg):
- if super().on_message(client, userdata, msg):
- return
-
- try:
- match = re.match(self.get_mqtt_topics(['data']), msg.topic)
- if not match:
- return
-
- device_id = match.group(1)
- subtopic = match.group(2)
-
- message = None
- if subtopic == 'data':
- message = TempHumDataPayload.unpack(msg.payload)
-
- if message and self._message_callback:
- self._message_callback(device_id, message)
-
- except Exception as e:
- self._logger.exception(str(e))
diff --git a/src/home/mqtt/util.py b/src/home/mqtt/util.py
deleted file mode 100644
index f71ffd8..0000000
--- a/src/home/mqtt/util.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import time
-
-
-def poll_tick(freq):
- t = time.time()
- while True:
- t += freq
- yield max(t - time.time(), 0)
diff --git a/src/home/temphum/__init__.py b/src/home/temphum/__init__.py
deleted file mode 100644
index 55a7e1f..0000000
--- a/src/home/temphum/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from .base import SensorType, TempHumSensor
-from .si7021 import Si7021
-from .dht12 import DHT12
-
-__all__ = [
- 'SensorType',
- 'TempHumSensor',
- 'create_sensor'
-]
-
-
-def create_sensor(type: SensorType, bus: int) -> TempHumSensor:
- if type == SensorType.Si7021:
- return Si7021(bus)
- elif type == SensorType.DHT12:
- return DHT12(bus)
- else:
- raise ValueError('unexpected sensor type')
diff --git a/src/home/temphum/dht12.py b/src/home/temphum/dht12.py
deleted file mode 100644
index d495766..0000000
--- a/src/home/temphum/dht12.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from .base import I2CTempHumSensor
-
-
-class DHT12(I2CTempHumSensor):
- i2c_addr = 0x5C
-
- def _measure(self):
- raw = self.bus.read_i2c_block_data(self.i2c_addr, 0, 5)
- if (raw[0] + raw[1] + raw[2] + raw[3]) & 0xff != raw[4]:
- raise ValueError("checksum error")
- return raw
-
- def temperature(self) -> float:
- raw = self._measure()
- temp = raw[2] + (raw[3] & 0x7f) * 0.1
- if raw[3] & 0x80:
- temp *= -1
- return temp
-
- def humidity(self) -> float:
- raw = self._measure()
- return raw[0] + raw[1] * 0.1
diff --git a/src/home/temphum/si7021.py b/src/home/temphum/si7021.py
deleted file mode 100644
index 6289e15..0000000
--- a/src/home/temphum/si7021.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from .base import I2CTempHumSensor
-
-
-class Si7021(I2CTempHumSensor):
- i2c_addr = 0x40
-
- def temperature(self) -> float:
- raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE3, 2)
- return 175.72 * (raw[0] << 8 | raw[1]) / 65536.0 - 46.85
-
- def humidity(self) -> float:
- raw = self.bus.read_i2c_block_data(self.i2c_addr, 0xE5, 2)
- return 125.0 * (raw[0] << 8 | raw[1]) / 65536.0 - 6.0
diff --git a/src/inverter_mqtt_receiver.py b/src/inverter_mqtt_receiver.py
deleted file mode 100755
index d40647e..0000000
--- a/src/inverter_mqtt_receiver.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python3
-import paho.mqtt.client as mqtt
-import re
-
-from home.mqtt import MqttBase
-from home.mqtt.payload.inverter import Status, Generation
-from home.database import InverterDatabase
-from home.config import config
-
-
-class MqttReceiver(MqttBase):
- def __init__(self):
- super().__init__(clean_session=False)
- self.database = InverterDatabase()
-
- def on_connect(self, client: mqtt.Client, userdata, flags, rc):
- super().on_connect(client, userdata, flags, rc)
- self._logger.info("subscribing to hk/#")
- client.subscribe('hk/#', qos=1)
-
- def on_message(self, client: mqtt.Client, userdata, msg):
- super().on_message(client, userdata, msg)
- try:
- match = re.match(r'(?:home|hk)/(\d+)/(status|gen)', msg.topic)
- if not match:
- return
-
- # FIXME string home_id must be supported
- home_id, what = int(match.group(1)), match.group(2)
- if what == 'gen':
- gen = Generation.unpack(msg.payload)
- self.database.add_generation(home_id, gen.time, gen.wh)
-
- elif what == 'status':
- s = Status.unpack(msg.payload)
- self.database.add_status(home_id,
- client_time=s.time,
- grid_voltage=int(s.grid_voltage*10),
- grid_freq=int(s.grid_freq * 10),
- ac_output_voltage=int(s.ac_output_voltage * 10),
- ac_output_freq=int(s.ac_output_freq * 10),
- ac_output_apparent_power=s.ac_output_apparent_power,
- ac_output_active_power=s.ac_output_active_power,
- output_load_percent=s.output_load_percent,
- battery_voltage=int(s.battery_voltage * 10),
- battery_voltage_scc=int(s.battery_voltage_scc * 10),
- battery_voltage_scc2=int(s.battery_voltage_scc2 * 10),
- battery_discharge_current=s.battery_discharge_current,
- battery_charge_current=s.battery_charge_current,
- battery_capacity=s.battery_capacity,
- inverter_heat_sink_temp=s.inverter_heat_sink_temp,
- mppt1_charger_temp=s.mppt1_charger_temp,
- mppt2_charger_temp=s.mppt2_charger_temp,
- pv1_input_power=s.pv1_input_power,
- pv2_input_power=s.pv2_input_power,
- pv1_input_voltage=int(s.pv1_input_voltage * 10),
- pv2_input_voltage=int(s.pv2_input_voltage * 10),
- mppt1_charger_status=s.mppt1_charger_status,
- mppt2_charger_status=s.mppt2_charger_status,
- battery_power_direction=s.battery_power_direction,
- dc_ac_power_direction=s.dc_ac_power_direction,
- line_power_direction=s.line_power_direction,
- load_connected=s.load_connected)
-
- except Exception as e:
- self._logger.exception(str(e))
-
-
-if __name__ == '__main__':
- config.load('inverter_mqtt_receiver')
-
- server = MqttReceiver()
- server.connect_and_loop()
-
diff --git a/src/inverter_mqtt_sender.py b/src/inverter_mqtt_sender.py
deleted file mode 100755
index fb2a2d8..0000000
--- a/src/inverter_mqtt_sender.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-import time
-import datetime
-import json
-import inverterd
-
-from home.config import config
-from home.mqtt import MqttBase, poll_tick
-from home.mqtt.payload.inverter import Status, Generation
-
-
-class MqttClient(MqttBase):
- def __init__(self):
- super().__init__()
-
- self._home_id = config['mqtt']['home_id']
-
- self._inverter = inverterd.Client()
- self._inverter.connect()
- self._inverter.format(inverterd.Format.SIMPLE_JSON)
-
- def poll_inverter(self):
- freq = int(config['mqtt']['inverter']['poll_freq'])
- gen_freq = int(config['mqtt']['inverter']['generation_poll_freq'])
-
- g = poll_tick(freq)
- gen_prev = 0
- while True:
- time.sleep(next(g))
-
- # read status
- now = time.time()
- try:
- raw = self._inverter.exec('get-status')
- except inverterd.InverterError as e:
- self._logger.error(f'inverter error: {str(e)}')
- # TODO send to server
- continue
-
- data = json.loads(raw)['data']
- status = Status(time=round(now), **data) # FIXME this will crash with 99% probability
-
- self._client.publish(f'hk/{self._home_id}/status',
- payload=status.pack(),
- qos=1)
-
- # read today's generation stat
- now = time.time()
- if gen_prev == 0 or now - gen_prev >= gen_freq:
- gen_prev = now
- today = datetime.date.today()
- try:
- raw = self._inverter.exec('get-day-generated', (today.year, today.month, today.day))
- except inverterd.InverterError as e:
- self._logger.error(f'inverter error: {str(e)}')
- # TODO send to server
- continue
-
- data = json.loads(raw)['data']
- gen = Generation(time=round(now), wh=data['wh'])
- self._client.publish(f'hk/{self._home_id}/gen',
- payload=gen.pack(),
- qos=1)
-
-
-if __name__ == '__main__':
- config.load('inverter_mqtt_sender')
-
- client = MqttClient()
- client.configure_tls()
- client.connect_and_loop(loop_forever=False)
- client.poll_inverter() \ 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 d31c3bf..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('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/pump_bot.py b/src/pump_bot.py
deleted file mode 100755
index de925db..0000000
--- a/src/pump_bot.py
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env python3
-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.relay.sunxi_h3_client import RelayClient
-from home.api.types import BotType
-
-config.load('pump_bot')
-
-bot.initialize()
-bot.lang.ru(
- start_message="Выберите команду на клавиатуре",
- unknown_command="Неизвестная команда",
-
- enable="Включить",
- enable_silently="Включить тихо",
- enabled="Включен ✅",
-
- disable="Выключить",
- disable_silently="Выключить тихо",
- disabled="Выключен ❌",
-
- status="Статус",
- done="Готово 👌",
- user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.',
- user_action_on="включил",
- user_action_off="выключил",
-)
-bot.lang.en(
- start_message="Select command on the keyboard",
- unknown_command="Unknown command",
-
- enable="Turn ON",
- enable_silently="Turn ON silently",
- enabled="Turned ON ✅",
-
- disable="Turn OFF",
- disable_silently="Turn OFF silently",
- disabled="Turned OFF ❌",
-
- status="Status",
- 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",
-)
-
-
-class UserAction(Enum):
- ON = 'on'
- OFF = 'off'
-
-
-def get_relay() -> RelayClient:
- relay = RelayClient(host=config['relay']['ip'], port=config['relay']['port'])
- relay.connect()
- return relay
-
-
-def on(ctx: bot.Context, silent=False) -> None:
- get_relay().on()
- ctx.reply(ctx.lang('done'))
- if not silent:
- notify(ctx.user, UserAction.ON)
-
-
-def off(ctx: bot.Context, silent=False) -> None:
- get_relay().off()
- ctx.reply(ctx.lang('done'))
- if not silent:
- notify(ctx.user, UserAction.OFF)
-
-
-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)
-
- bot.notify_all(text_getter, exclude=(user.id,))
-
-
-@bot.handler(message='enable')
-def enable_handler(ctx: bot.Context) -> None:
- on(ctx)
-
-
-@bot.handler(message='enable_silently')
-def enable_s_handler(ctx: bot.Context) -> None:
- on(ctx, True)
-
-
-@bot.handler(message='disable')
-def disable_handler(ctx: bot.Context) -> None:
- off(ctx)
-
-
-@bot.handler(message='disable_silently')
-def disable_s_handler(ctx: bot.Context) -> None:
- off(ctx, True)
-
-
-@bot.handler(message='status')
-def status(ctx: bot.Context) -> None:
- ctx.reply(
- ctx.lang('enabled') if get_relay().status() == 'on' else ctx.lang('disabled')
- )
-
-
-@bot.defaultreplymarkup
-def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
- buttons = [
- [ctx.lang('enable'), ctx.lang('disable')],
- ]
-
- if ctx.user_id in config['bot']['silent_users']:
- buttons.append([ctx.lang('enable_silently'), ctx.lang('disable_silently')])
-
- buttons.append([ctx.lang('status')])
-
- return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
-
-
-if __name__ == '__main__':
- bot.enable_logging(BotType.PUMP)
- bot.run()
diff --git a/src/relay_mqtt_bot.py b/src/relay_mqtt_bot.py
deleted file mode 100755
index ebbff82..0000000
--- a/src/relay_mqtt_bot.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-from enum import Enum
-from typing import Optional
-from telegram import ReplyKeyboardMarkup
-from functools import partial
-
-from home.config import config
-from home.telegram import bot
-from home.mqtt import MqttRelay, MqttRelayState
-from home.mqtt.esp import MqttEspDevice
-from home.mqtt.payload import MqttPayload
-from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload
-
-
-config.load('relay_mqtt_bot')
-
-bot.initialize()
-bot.lang.ru(
- start_message="Выберите команду на клавиатуре",
- unknown_command="Неизвестная команда",
- done="Готово 👌",
-)
-bot.lang.en(
- start_message="Select command on the keyboard",
- unknown_command="Unknown command",
- done="Done 👌",
-)
-
-
-type_emojis = {
- 'lamp': '💡'
-}
-status_emoji = {
- 'on': '✅',
- 'off': '❌'
-}
-mqtt_relay: Optional[MqttRelay] = None
-relay_states: dict[str, MqttRelayState] = {}
-
-
-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
- if home_id not in relay_states:
- relay_states[home_id] = MqttRelayState()
- relay_states[home_id].update(**kwargs)
-
-
-def enable_handler(home_id: str, ctx: bot.Context) -> None:
- mqtt_relay.set_power(home_id, True)
- ctx.reply(ctx.lang('done'))
-
-
-def disable_handler(home_id: str, ctx: bot.Context) -> None:
- mqtt_relay.set_power(home_id, False)
- ctx.reply(ctx.lang('done'))
-
-
-def start(ctx: bot.Context) -> None:
- ctx.reply(ctx.lang('start_message'))
-
-
-@bot.exceptionhandler
-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 i in UserAction]
- buttons.append(row)
- return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
-
-
-if __name__ == '__main__':
- devices = []
- for device_id, data in config['relays'].items():
- devices.append(MqttEspDevice(id=device_id,
- secret=data['secret']))
- labels = data['labels']
- bot.lang.ru(**{device_id: labels['ru']})
- bot.lang.en(**{device_id: labels['en']})
-
- type_emoji = type_emojis[data['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))
-
- mqtt_relay = MqttRelay(devices=devices)
- mqtt_relay.set_message_callback(on_mqtt_message)
- mqtt_relay.configure_tls()
- mqtt_relay.connect_and_loop(loop_forever=False)
-
- # bot.enable_logging(BotType.RELAY_MQTT)
- bot.run(start_handler=start)
-
- mqtt_relay.disconnect()
diff --git a/src/relay_mqtt_http_proxy.py b/src/relay_mqtt_http_proxy.py
deleted file mode 100755
index 098facc..0000000
--- a/src/relay_mqtt_http_proxy.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-from home import http
-from home.config import config
-from home.mqtt import MqttRelay, MqttRelayState
-from home.mqtt.esp import MqttEspDevice
-from home.mqtt.payload import MqttPayload
-from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload
-from typing import Optional
-
-mqtt_relay: Optional[MqttRelay] = None
-relay_states: dict[str, MqttRelayState] = {}
-
-
-def on_mqtt_message(device_id, 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):
- device_id = req.match_info['id']
- device_secret = req.query['secret']
-
- if enable is None:
- if device_id in relay_states and relay_states[device_id].ever_updated:
- cur_state = relay_states[device_id].enabled
- else:
- cur_state = False
- enable = not cur_state
-
- mqtt_relay.set_power(device_id, enable, device_secret)
- 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('relay_mqtt_http_proxy')
-
- mqtt_relay = MqttRelay(devices=[MqttEspDevice(id=device_id) for device_id in config.get('relay.devices')])
- mqtt_relay.configure_tls()
- mqtt_relay.set_message_callback(on_mqtt_message)
- mqtt_relay.connect_and_loop(loop_forever=False)
-
- proxy = RelayMqttHttpProxy(config.get_addr('server.listen'))
- try:
- proxy.run()
- except KeyboardInterrupt:
- mqtt_relay.disconnect()
diff --git a/src/sensors_mqtt_sender.py b/src/sensors_mqtt_sender.py
deleted file mode 100755
index 87a28ca..0000000
--- a/src/sensors_mqtt_sender.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env python3
-import time
-import json
-
-from home.util import parse_addr, MySimpleSocketClient
-from home.mqtt import MqttBase, poll_tick
-from home.mqtt.payload.sensors import Temperature
-from home.config import config
-
-
-class MqttClient(MqttBase):
- def __init__(self):
- super().__init__(self)
- self._home_id = config['mqtt']['home_id']
-
- def poll(self):
- freq = int(config['mqtt']['sensors']['poll_freq'])
- self._logger.debug(f'freq={freq}')
-
- g = poll_tick(freq)
- while True:
- time.sleep(next(g))
- for k, v in config['mqtt']['sensors']['si7021'].items():
- host, port = parse_addr(v['addr'])
- self.publish_si7021(host, port, k)
-
- def publish_si7021(self, host: str, port: int, name: str):
- self._logger.debug(f"publish_si7021/{name}: {host}:{port}")
-
- try:
- now = time.time()
- socket = MySimpleSocketClient(host, port)
-
- socket.write('read')
- response = json.loads(socket.read().strip())
-
- temp = response['temp']
- humidity = response['humidity']
-
- self._logger.debug(f'publish_si7021/{name}: temp={temp} humidity={humidity}')
-
- pld = Temperature(time=round(now),
- temp=temp,
- rh=humidity)
- self._client.publish(f'hk/{self._home_id}/si7021/{name}',
- payload=pld.pack(),
- qos=1)
- except Exception as e:
- self._logger.exception(e)
-
-
-if __name__ == '__main__':
- config.load('sensors_mqtt_sender')
-
- client = MqttClient()
- client.configure_tls()
- client.connect_and_loop(loop_forever=False)
- client.poll()
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
new file mode 100644
index 0000000..88f9169
--- /dev/null
+++ b/systemd/inverter_mqtt_receiver.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Inverter MQTT receiver
+After=clickhouse-server.service
+
+[Service]
+User=user
+Group=user
+Restart=on-failure
+ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py receiver
+WorkingDirectory=/home/user
+
+[Install]
+WantedBy=multi-user.target \ No newline at end of file
diff --git a/systemd/inverter_mqtt_sender.service b/systemd/inverter_mqtt_sender.service
index e3925f6..bf6ab61 100644
--- a/systemd/inverter_mqtt_sender.service
+++ b/systemd/inverter_mqtt_sender.service
@@ -6,7 +6,7 @@ After=inverterd.service
User=user
Group=user
Restart=on-failure
-ExecStart=/home/user/homekit/src/inverter_mqtt_sender.py
+ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py sender
WorkingDirectory=/home/user
[Install]
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 addd819..0000000
--- a/systemd/ipcam_rtsp2hls@.service
+++ /dev/null
@@ -1,14 +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
-
-[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/sensors_mqtt_receiver.service b/systemd/sensors_mqtt_receiver.service
index e67c112..5b9ff6a 100644
--- a/systemd/sensors_mqtt_receiver.service
+++ b/systemd/sensors_mqtt_receiver.service
@@ -1,12 +1,12 @@
[Unit]
-Description=sensors mqtt receiver
+Description=temphum mqtt receiver
After=network.target
[Service]
User=user
Group=user
Restart=on-failure
-ExecStart=python3 /home/user/home/src/sensors_mqtt_receiver.py
+ExecStart=python3 /home/user/home/src/temphum_mqtt_receiver.py
WorkingDirectory=/home/user
[Install]
diff --git a/systemd/sensors_mqtt_sender.service b/systemd/sensors_mqtt_sender.service
deleted file mode 100644
index a271d72..0000000
--- a/systemd/sensors_mqtt_sender.service
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=Sensors MQTT sender
-After=temphumd.service
-
-[Service]
-User=user
-Group=user
-Restart=on-failure
-ExecStart=/home/user/homekit/src/sensors_mqtt_sender.py
-WorkingDirectory=/home/user
-
-[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/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
new file mode 100755
index 0000000..6c02d75
--- /dev/null
+++ b/test/mqtt_relay_server_util.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+import __py_include
+
+from homekit.config import config
+
+
+if __name__ == '__main__':
+ 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
new file mode 100755
index 0000000..394bbe8
--- /dev/null
+++ b/test/mqtt_relay_util.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+import __py_include
+
+from argparse import ArgumentParser
+from homekit.config import config
+from homekit.mqtt.relay import MQTTRelayController
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument('--on', action='store_true')
+ parser.add_argument('--off', action='store_true')
+ parser.add_argument('--stat', action='store_true')
+
+ config.load_app('test_mqtt_relay', parser=parser)
+ arg = parser.parse_args()
+
+ relay = MQTTRelayController('test')
+ relay.connect_and_loop(loop_forever=False)
+
+ if arg.on:
+ relay.set_power(True)
+
+ elif arg.off:
+ relay.set_power(False)
+
+ elif arg.stat:
+ relay.send_stat(dict(
+ 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 c8bd546..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):
@@ -28,7 +25,7 @@ if __name__ == '__main__':
parser.add_argument('--decr', type=str)
# parser.add_argument('--dump-config', action='store_true')
- args = config.load('test_amixer', parser=parser)
+ args = config.load_app('test_amixer', parser=parser)
# if args.dump_config:
# print(config.data)
diff --git a/test/test_api.py b/test/test_api.py
index 1f6361c..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('test_api')
+ 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 27ce379..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()
@@ -21,8 +15,8 @@ if __name__ == '__main__':
parser.add_argument('--status', action='store_true',
help='print status and exit')
- arg = config.load(False, parser=parser)
- cam = esp32.WebClient(addr=parse_addr(arg.addr))
+ arg = config.load_app(False, parser=parser)
+ 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 3b1c6b0..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,
@@ -372,5 +361,5 @@ def main():
if __name__ == '__main__':
- config.load('test_inverter_monitor')
+ config.load_app('test_inverter_monitor')
main()
diff --git a/test/test_ipcam_server_cleanup.py b/test/test_ipcam_server_cleanup.py
index b7eb23a..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__)
@@ -77,5 +71,5 @@ def cleanup_job():
if __name__ == '__main__':
- config.load('ipcam_server')
+ config.load_app('ipcam_server')
cleanup_job()
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 cbd3ca2..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__)
@@ -64,17 +57,17 @@ def api_success_handler(response, name, req: RequestParams):
if __name__ == '__main__':
- config.load('test_record_upload')
+ config.load_app('test_record_upload')
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 e68c6f8..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
@@ -56,10 +50,10 @@ def hits_sender():
if __name__ == '__main__':
- config.load('test_api')
+ 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 705e534..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():
@@ -20,7 +13,7 @@ async def main():
if __name__ == '__main__':
- config.load('test_telegram_aio_send_photo')
+ config.load_app('test_telegram_aio_send_photo')
loop = asyncio.get_event_loop()
asyncio.ensure_future(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/mcuota.py b/tools/mcuota.py
deleted file mode 100755
index 46968a8..0000000
--- a/tools/mcuota.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/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__)), '..')
- )
-])
-
-from time import sleep
-from argparse import ArgumentParser
-from src.home.config import config
-from src.home.mqtt import MqttRelay
-from src.home.mqtt.esp import MqttEspDevice
-
-
-def guess_filename(product: str, build_target: str):
- return os.path.join(
- products_dir,
- product,
- '.pio',
- 'build',
- build_target,
- 'firmware.bin'
- )
-
-
-def relayctl_publish_ota(filename: str,
- device_id: str,
- home_secret: str,
- qos: int):
- global stop
-
- def published():
- global stop
- stop = True
-
- mqtt_relay = MqttRelay(devices=MqttEspDevice(id=device_id, secret=home_secret))
- mqtt_relay.configure_tls()
- mqtt_relay.connect_and_loop(loop_forever=False)
- mqtt_relay.push_ota(device_id, filename, published, qos)
- while not stop:
- sleep(0.1)
- mqtt_relay.disconnect()
-
-
-stop = False
-products = {
- 'relayctl': {
- 'build_target': 'esp12e',
- 'callback': relayctl_publish_ota
- }
-}
-
-products_dir = os.path.join(
- os.path.dirname(__file__),
- '..',
- 'platformio'
-)
-
-
-def main():
- parser = ArgumentParser()
- parser.add_argument('--filename', type=str)
- parser.add_argument('--device-id', type=str, required=True)
- parser.add_argument('--product', type=str, required=True)
- parser.add_argument('--qos', type=int, default=1)
-
- config.load('mcuota_push', parser=parser)
- arg = parser.parse_args()
-
- if arg.product not in products:
- raise ValueError(f'invalid product: \'{arg.product}\' not found')
-
- if arg.device_id not in config['mqtt']['home_secrets']:
- raise ValueError(f'home_secret for home {arg.device_id} not found in config!')
-
- filename = arg.filename if arg.filename else guess_filename(arg.product, products[arg.product]['build_target'])
- if not os.path.exists(filename):
- raise OSError(f'file \'{filename}\' does not exists')
-
- print('Please confirm following OTA params.')
- print('')
- print(f' Device ID: {arg.device_id}')
- print(f' Product: {arg.product}')
- print(f'Firmware file: {filename}')
- print('')
- input('Press any key to continue or Ctrl+C to abort.')
-
- products[arg.product]['callback'](filename, arg.device_id, config['mqtt']['home_secrets'][arg.device_id], qos=arg.qos)
-
-
-if __name__ == '__main__':
- try:
- main()
- except Exception as e:
- print(str(e), file=sys.stderr)
- sys.exit(1)
diff --git a/tools/mcuota.sh b/tools/mcuota.sh
deleted file mode 100755
index b2e7910..0000000
--- a/tools/mcuota.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
-
-. "$DIR/lib.bash"
-
-if [ -d "$DIR/../venv" ]; then
- echoinfo "activating python venv"
- . "$DIR/../venv/bin/activate"
-else
- echowarn "python venv not found"
-fi
-
-"$DIR/mcuota.py" "$@" \ No newline at end of file
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=