From f29e139cbb7e4a4d539cba6e894ef4a6acd312d6 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 31 May 2023 09:22:00 +0300 Subject: [PATCH 01/51] WIP: big refactoring --- .gitignore | 1 + misc/openwrt/etc/rc.local | 2 +- platformio/common/libs/main/homekit/main.cpp | 21 +- platformio/common/libs/main/homekit/main.h | 4 + platformio/common/libs/main/library.json | 2 +- .../common/libs/mqtt/homekit/mqtt/module.cpp | 2 +- .../common/libs/mqtt/homekit/mqtt/module.h | 15 +- .../common/libs/mqtt/homekit/mqtt/mqtt.cpp | 29 +- platformio/common/libs/mqtt/library.json | 2 +- .../homekit/mqtt/module/diagnostics.cpp | 11 +- .../homekit/mqtt/module/diagnostics.h | 5 +- .../libs/mqtt_module_diagnostics/library.json | 4 +- .../homekit/mqtt/module/ota.cpp | 12 +- .../mqtt_module_ota/homekit/mqtt/module/ota.h | 7 +- .../common/libs/mqtt_module_ota/library.json | 4 +- .../homekit/mqtt/module/relay.cpp | 27 +- .../homekit/mqtt/module/relay.h | 10 +- .../libs/mqtt_module_relay/library.json | 2 +- .../homekit/mqtt/module/temphum.cpp | 2 +- .../homekit/mqtt/module/temphum.h | 2 +- .../libs/mqtt_module_temphum/library.json | 4 +- platformio/temphum_relayctl/src/main.cpp | 1 + requirements.txt | 21 +- src/camera_node.py | 2 +- src/esp32_capture.py | 4 +- src/esp32cam_capture_diff_node.py | 8 +- src/esp_mqtt_util.py | 42 --- src/gpiorelayd.py | 2 +- src/home/audio/amixer.py | 2 +- src/home/config/__init__.py | 14 +- src/home/config/_configs.py | 55 +++ src/home/config/config.py | 337 +++++++++++++----- src/home/database/clickhouse.py | 2 +- src/home/database/sqlite.py | 25 +- src/home/inverter/config.py | 13 + src/home/media/__init__.py | 1 + src/home/mqtt/__init__.py | 11 +- src/home/mqtt/_config.py | 165 +++++++++ src/home/mqtt/_module.py | 70 ++++ src/home/mqtt/{mqtt.py => _mqtt.py} | 52 +-- src/home/mqtt/_node.py | 92 +++++ .../{payload/base_payload.py => _payload.py} | 4 +- src/home/mqtt/_util.py | 15 + src/home/mqtt/_wrapper.py | 59 +++ src/home/mqtt/esp.py | 106 ------ .../{payload/esp.py => module/diagnostics.py} | 56 ++- src/home/mqtt/module/inverter.py | 195 ++++++++++ src/home/mqtt/module/ota.py | 77 ++++ src/home/mqtt/module/relay.py | 92 +++++ src/home/mqtt/module/temphum.py | 82 +++++ src/home/mqtt/payload/__init__.py | 1 - src/home/mqtt/payload/inverter.py | 73 ---- src/home/mqtt/payload/relay.py | 22 -- src/home/mqtt/payload/sensors.py | 20 -- src/home/mqtt/payload/temphum.py | 15 - src/home/mqtt/relay.py | 71 ---- src/home/mqtt/temphum.py | 54 --- src/home/mqtt/util.py | 8 - src/home/pio/products.py | 4 - src/home/telegram/_botcontext.py | 19 +- src/home/telegram/bot.py | 149 ++++---- src/home/telegram/config.py | 75 ++++ src/home/temphum/__init__.py | 19 +- src/home/temphum/base.py | 24 +- src/home/temphum/dht12.py | 22 -- src/home/temphum/i2c.py | 52 +++ src/home/temphum/si7021.py | 13 - src/home/util.py | 70 +++- src/inverter_bot.py | 99 +++-- src/inverter_mqtt_receiver.py | 74 ---- src/inverter_mqtt_sender.py | 72 ---- src/inverter_mqtt_util.py | 25 ++ src/ipcam_server.py | 2 +- src/mqtt_node_util.py | 63 ++++ src/openwrt_log_analyzer.py | 2 +- src/openwrt_logger.py | 2 +- src/pio_ini.py | 10 +- src/polaris_kettle_bot.py | 8 +- src/polaris_kettle_util.py | 6 +- src/pump_bot.py | 154 +++++++- src/pump_mqtt_bot.py | 26 +- src/relay_mqtt_bot.py | 144 +++++--- src/relay_mqtt_http_proxy.py | 49 ++- src/sensors_bot.py | 2 +- src/sensors_mqtt_sender.py | 58 --- src/sound_bot.py | 12 +- src/sound_node.py | 2 +- src/sound_sensor_node.py | 6 +- src/sound_sensor_server.py | 8 +- src/ssh_tunnels_config_util.py | 4 +- src/temphum_mqtt_node.py | 78 ++++ ...t_receiver.py => temphum_mqtt_receiver.py} | 24 +- src/temphum_smbus_util.py | 3 +- src/temphumd.py | 7 +- src/test_new_config.py | 12 + src/web_api.py | 2 +- systemd/inverter_mqtt_receiver.service | 13 + systemd/inverter_mqtt_sender.service | 2 +- systemd/ipcam_rtsp2hls@.service | 2 + systemd/sensors_mqtt_receiver.service | 4 +- systemd/sensors_mqtt_sender.service | 13 - test/mqtt_relay_server_util.py | 17 + test/mqtt_relay_util.py | 38 ++ test/test_amixer.py | 2 +- test/test_api.py | 2 +- test/test_esp32_cam.py | 6 +- test/test_inverter_monitor.py | 2 +- test/test_ipcam_server_cleanup.py | 2 +- test/test_record_upload.py | 6 +- test/test_send_fake_sound_hit.py | 4 +- test/test_sound_server_api.py | 2 +- test/test_telegram_aio_send_photo.py | 2 +- tools/mcuota.py | 98 ----- tools/mcuota.sh | 14 - 114 files changed, 2336 insertions(+), 1331 deletions(-) delete mode 100755 src/esp_mqtt_util.py create mode 100644 src/home/config/_configs.py create mode 100644 src/home/inverter/config.py create mode 100644 src/home/mqtt/_config.py create mode 100644 src/home/mqtt/_module.py rename src/home/mqtt/{mqtt.py => _mqtt.py} (58%) create mode 100644 src/home/mqtt/_node.py rename src/home/mqtt/{payload/base_payload.py => _payload.py} (99%) create mode 100644 src/home/mqtt/_util.py create mode 100644 src/home/mqtt/_wrapper.py delete mode 100644 src/home/mqtt/esp.py rename src/home/mqtt/{payload/esp.py => module/diagnostics.py} (54%) create mode 100644 src/home/mqtt/module/inverter.py create mode 100644 src/home/mqtt/module/ota.py create mode 100644 src/home/mqtt/module/relay.py create mode 100644 src/home/mqtt/module/temphum.py delete mode 100644 src/home/mqtt/payload/__init__.py delete mode 100644 src/home/mqtt/payload/inverter.py delete mode 100644 src/home/mqtt/payload/relay.py delete mode 100644 src/home/mqtt/payload/sensors.py delete mode 100644 src/home/mqtt/payload/temphum.py delete mode 100644 src/home/mqtt/relay.py delete mode 100644 src/home/mqtt/temphum.py delete mode 100644 src/home/mqtt/util.py create mode 100644 src/home/telegram/config.py delete mode 100644 src/home/temphum/dht12.py create mode 100644 src/home/temphum/i2c.py delete mode 100644 src/home/temphum/si7021.py delete mode 100755 src/inverter_mqtt_receiver.py delete mode 100755 src/inverter_mqtt_sender.py create mode 100755 src/inverter_mqtt_util.py create mode 100755 src/mqtt_node_util.py delete mode 100755 src/sensors_mqtt_sender.py create mode 100755 src/temphum_mqtt_node.py rename src/{sensors_mqtt_receiver.py => temphum_mqtt_receiver.py} (70%) create mode 100755 src/test_new_config.py create mode 100644 systemd/inverter_mqtt_receiver.service delete mode 100644 systemd/sensors_mqtt_sender.service create mode 100755 test/mqtt_relay_server_util.py create mode 100755 test/mqtt_relay_util.py delete mode 100755 tools/mcuota.py delete mode 100755 tools/mcuota.sh diff --git a/.gitignore b/.gitignore index 5f65bca..4ffc1b1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ platformio.ini CMakeListsPrivate.txt /platformio/*/CMakeLists.txt /platformio/*/CMakeListsPrivate.txt +/platformio/*/.gitignore *.swp /localwebsite/vendor 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/platformio/common/libs/main/homekit/main.cpp b/platformio/common/libs/main/homekit/main.cpp index fd08925..816c764 100644 --- a/platformio/common/libs/main/homekit/main.cpp +++ b/platformio/common/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,25 +106,31 @@ void setup() { } #ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY switch (working_mode) { case WorkingMode::RECOVERY: wifiHotspot(); break; 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("."); @@ -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/platformio/common/libs/main/homekit/main.h index a503dd0..78a0695 100644 --- a/platformio/common/libs/main/homekit/main.h +++ b/platformio/common/libs/main/homekit/main.h @@ -10,8 +10,10 @@ #include #include #ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY #include #endif +#endif #include #include @@ -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/platformio/common/libs/main/library.json b/platformio/common/libs/main/library.json index 04eedab..728d4f8 100644 --- a/platformio/common/libs/main/library.json +++ b/platformio/common/libs/main/library.json @@ -1,6 +1,6 @@ { "name": "homekit_main", - "version": "1.0.8", + "version": "1.0.10", "build": { "flags": "-I../../include" }, diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp b/platformio/common/libs/mqtt/homekit/mqtt/module.cpp index e78ff12..0ac7637 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp +++ b/platformio/common/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/platformio/common/libs/mqtt/homekit/mqtt/module.h index e4a01f8..0a328f3 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/module.h +++ b/platformio/common/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/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp index cb2cea7..aa769a5 100644 --- a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp +++ b/platformio/common/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/library.json b/platformio/common/libs/mqtt/library.json index d1ad420..f3f2504 100644 --- a/platformio/common/libs/mqtt/library.json +++ b/platformio/common/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/platformio/common/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/platformio/common/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/platformio/common/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/platformio/common/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/platformio/common/libs/mqtt_module_diagnostics/library.json index 8df306d..a3d3244 100644 --- a/platformio/common/libs/mqtt_module_diagnostics/library.json +++ b/platformio/common/libs/mqtt_module_diagnostics/library.json @@ -1,10 +1,10 @@ { "name": "homekit_mqtt_module_diagnostics", - "version": "1.0.1", + "version": "1.0.2", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt" + "homekit_mqtt": "file://../common/libs/mqtt" } } diff --git a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp b/platformio/common/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/platformio/common/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(&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/platformio/common/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/platformio/common/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/platformio/common/libs/mqtt_module_ota/library.json b/platformio/common/libs/mqtt_module_ota/library.json index 30db7d2..4f40a47 100644 --- a/platformio/common/libs/mqtt_module_ota/library.json +++ b/platformio/common/libs/mqtt_module_ota/library.json @@ -1,11 +1,11 @@ { "name": "homekit_mqtt_module_ota", - "version": "1.0.2", + "version": "1.0.5", "build": { "flags": "-I../../include" }, "dependencies": { "homekit_led": "file://../common/libs/led", - "homekit_mqtt": "file://../common/libs/mqtt" + "homekit_mqtt": "file://../common/libs/mqtt" } } diff --git a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp b/platformio/common/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/platformio/common/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(&resp), sizeof(resp)); } } + diff --git a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h b/platformio/common/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/platformio/common/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/platformio/common/libs/mqtt_module_relay/library.json b/platformio/common/libs/mqtt_module_relay/library.json index e71cf95..6cbbfb0 100644 --- a/platformio/common/libs/mqtt_module_relay/library.json +++ b/platformio/common/libs/mqtt_module_relay/library.json @@ -1,6 +1,6 @@ { "name": "homekit_mqtt_module_relay", - "version": "1.0.3", + "version": "1.0.5", "build": { "flags": "-I../../include" }, diff --git a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp b/platformio/common/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/platformio/common/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/platformio/common/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/platformio/common/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/platformio/common/libs/mqtt_module_temphum/library.json b/platformio/common/libs/mqtt_module_temphum/library.json index 9bb8cf1..068debd 100644 --- a/platformio/common/libs/mqtt_module_temphum/library.json +++ b/platformio/common/libs/mqtt_module_temphum/library.json @@ -1,11 +1,11 @@ { "name": "homekit_mqtt_module_temphum", - "version": "1.0.9", + "version": "1.0.10", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt", + "homekit_mqtt": "file://../common/libs/mqtt", "homekit_temphum": "file://../common/libs/temphum" } } diff --git a/platformio/temphum_relayctl/src/main.cpp b/platformio/temphum_relayctl/src/main.cpp index 0b05316..7f0945e 100644 --- a/platformio/temphum_relayctl/src/main.cpp +++ b/platformio/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/requirements.txt b/requirements.txt index 46f9b8c..4595dea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,24 @@ 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 +Pillow==9.5.0 # for polaris kettle protocol implementation -cryptography==38.0.4 -zeroconf==0.39.4 \ No newline at end of file +cryptography==41.0.1 +zeroconf==0.64.1 \ No newline at end of file diff --git a/src/camera_node.py b/src/camera_node.py index d175e17..3f2c5a4 100755 --- a/src/camera_node.py +++ b/src/camera_node.py @@ -65,7 +65,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/esp32_capture.py b/src/esp32_capture.py index 4a9ce10..0441565 100755 --- a/src/esp32_capture.py +++ b/src/esp32_capture.py @@ -5,7 +5,7 @@ import os.path from argparse import ArgumentParser from home.camera.esp32 import WebClient -from home.util import parse_addr, Addr +from home.util import Addr from apscheduler.schedulers.asyncio import AsyncIOScheduler from datetime import datetime from typing import Optional @@ -50,7 +50,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/src/esp32cam_capture_diff_node.py index 4363e9e..59482f7 100755 --- a/src/esp32cam_capture_diff_node.py +++ b/src/esp32cam_capture_diff_node.py @@ -7,7 +7,7 @@ import home.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 home.util import Addr, send_datagram, stringify from apscheduler.schedulers.asyncio import AsyncIOScheduler from typing import Optional @@ -34,11 +34,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 +76,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/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 index 85015a7..f1a9e57 100755 --- a/src/gpiorelayd.py +++ b/src/gpiorelayd.py @@ -13,7 +13,7 @@ if __name__ == '__main__': if not os.getegid() == 0: sys.exit('Must be run as root.') - config.load() + config.load_app() try: s = RelayServer(pinname=config.get('relayd.pin'), diff --git a/src/home/audio/amixer.py b/src/home/audio/amixer.py index 53e6bce..5133c97 100644 --- a/src/home/audio/amixer.py +++ b/src/home/audio/amixer.py @@ -1,6 +1,6 @@ import subprocess -from ..config import config +from ..config import app_config as config from threading import Lock from typing import Union, List diff --git a/src/home/config/__init__.py b/src/home/config/__init__.py index cc9c091..2fa5214 100644 --- a/src/home/config/__init__.py +++ b/src/home/config/__init__.py @@ -1 +1,13 @@ -from .config import ConfigStore, config, is_development_mode, setup_logging +from .config import ( + Config, + ConfigUnit, + AppConfigUnit, + Translation, + config, + is_development_mode, + setup_logging +) +from ._configs import ( + LinuxBoardsConfig, + ServicesListConfig +) \ No newline at end of file diff --git a/src/home/config/_configs.py b/src/home/config/_configs.py new file mode 100644 index 0000000..3a1aae5 --- /dev/null +++ b/src/home/config/_configs.py @@ -0,0 +1,55 @@ +from .config import ConfigUnit +from typing import Optional + + +class ServicesListConfig(ConfigUnit): + NAME = 'services_list' + + @staticmethod + def schema() -> Optional[dict]: + return { + 'type': 'list', + 'empty': False, + 'schema': { + 'type': 'string' + } + } + + +class LinuxBoardsConfig(ConfigUnit): + NAME = 'linux_boards' + + @staticmethod + def schema() -> 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} + } + }, + }, + } + } diff --git a/src/home/config/config.py b/src/home/config/config.py index 4681685..aef9ee7 100644 --- a/src/home/config/config.py +++ b/src/home/config/config.py @@ -1,58 +1,256 @@ -import toml import yaml import logging import os +import pprint -from os.path import join, isdir, isfile -from typing import Optional, Any, MutableMapping +from abc import ABC +from cerberus import Validator, DocumentError +from typing import Optional, Any, MutableMapping, Union from argparse import ArgumentParser -from ..util import parse_addr +from enum import Enum, auto +from os.path import join, isdir, isfile +from ..util import Addr -def _get_config_path(name: str) -> str: - formats = ['toml', 'yaml'] +CONFIG_DIRECTORIES = ( + join(os.environ['HOME'], '.config', 'homekit'), + '/etc/homekit' +) - 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 RootSchemaType(Enum): + DEFAULT = auto() + DICT = auto() + LIST = auto() -class ConfigStore: - data: MutableMapping[str, Any] - app_name: Optional[str] +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) + + 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' + + 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') + + @staticmethod + def schema() -> Optional[dict]: + return None + + 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': { + 'logging': {'type': 'bool'} + } + } + + 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 + + if rst == RootSchemaType.DICT: + v = Validator({'document': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': schema + }}) + result = v.validate({'document': self._data}) + elif rst == RootSchemaType.LIST: + v = Validator({'document': schema}) + result = v.validate({'document': self._data}) + else: + v = Validator(schema) + result = v.validate(self._data) + # pprint.pprint(self._data) + if not result: + # pprint.pprint(v.errors) + raise DocumentError(f'{self.__class__.__name__}: failed to validate data:\n{pprint.pformat(v.errors)}') + try: + self.custom_validator(self._data) + except Exception as e: + raise 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: + 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: + 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: + 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.data = {} self.app_name = None + self.app_config = AppConfigUnit() - def load(self, name: Optional[str] = None, - use_cli=True, - parser: ArgumentParser = None): - self.app_name = name + def load_app(self, + name: Optional[Union[str, AppConfigUnit, bool]] = None, + use_cli=True, + parser: ArgumentParser = None, + no_config=False): + global app_config - if (name is None) and (not use_cli): + if issubclass(name, AppConfigUnit) or name == AppConfigUnit: + self.app_name = name.NAME + self.app_config = name() + app_config = self.app_config + else: + self.app_name = name if isinstance(name, str) else None + + if self.app_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 - + no_config = name is False or no_config path = None + if use_cli: if parser is None: parser = ArgumentParser() @@ -68,75 +266,38 @@ class ConfigStore: path = args.config if args.verbose: - log_verbose = True + self.app_config.logging_set_verbose() if args.log_file: - log_file = args.log_file + self.app_config.logging_set_file(args.log_file) if args.log_default_fmt: - log_default_fmt = args.log_default_fmt + self.app_config.logging_set_fmt(args.log_default_fmt) - if not no_config and path is None: - path = _get_config_path(name) + if not isinstance(name, ConfigUnit): + if not no_config and path is None: + path = ConfigUnit.get_config_path(name=self.app_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 not no_config: + self.app_config.load_from(path) - 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) + 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 - 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() +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) and ('verbose' in config['logging']) and (config['logging']['verbose'] is 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=False): +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 diff --git a/src/home/database/clickhouse.py b/src/home/database/clickhouse.py index ca81628..d0ec283 100644 --- a/src/home/database/clickhouse.py +++ b/src/home/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/sqlite.py b/src/home/database/sqlite.py index bfba929..8c6145c 100644 --- a/src/home/database/sqlite.py +++ b/src/home/database/sqlite.py @@ -5,24 +5,27 @@ import logging 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( + os.environ['HOME'], + '.config', + 'homekit', + 'data', + 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: - if not name: - name = config.app_name - if not dbname: - dbname = name - db_path = _get_database_path(name, dbname) + def __init__(self, name=None, check_same_thread=False): + if name is None: + name = config.app_config['database_name'] + database_path = _get_database_path(name) + 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/inverter/config.py b/src/home/inverter/config.py new file mode 100644 index 0000000..62b8859 --- /dev/null +++ b/src/home/inverter/config.py @@ -0,0 +1,13 @@ +from ..config import ConfigUnit +from typing import Optional + + +class InverterdConfig(ConfigUnit): + NAME = 'inverterd' + + @staticmethod + def schema() -> Optional[dict]: + return { + 'remote_addr': {'type': 'string'}, + 'local_addr': {'type': 'string'}, + } \ No newline at end of file diff --git a/src/home/media/__init__.py b/src/home/media/__init__.py index 976c990..6923105 100644 --- a/src/home/media/__init__.py +++ b/src/home/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/mqtt/__init__.py b/src/home/mqtt/__init__.py index 982e2b6..707d59c 100644 --- a/src/home/mqtt/__init__.py +++ b/src/home/mqtt/__init__.py @@ -1,4 +1,7 @@ -from .mqtt import MqttBase -from .util import poll_tick -from .relay import MqttRelay, MqttRelayState -from .temphum import MqttTempHum \ No newline at end of file +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/src/home/mqtt/_config.py b/src/home/mqtt/_config.py new file mode 100644 index 0000000..f9047b4 --- /dev/null +++ b/src/home/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' + + @staticmethod + def schema() -> 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' + + @staticmethod + def schema() -> 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/src/home/mqtt/_module.py b/src/home/mqtt/_module.py new file mode 100644 index 0000000..80f27bb --- /dev/null +++ b/src/home/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/src/home/mqtt/_mqtt.py similarity index 58% rename from src/home/mqtt/mqtt.py rename to src/home/mqtt/_mqtt.py index 4acd4f6..746ae2e 100644 --- a/src/home/mqtt/mqtt.py +++ b/src/home/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,15 +29,17 @@ 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() - def configure_tls(self): + self._client.username_pw_set(creds.username, creds.password) + + def _configure_tls(self): ca_certs = os.path.realpath(os.path.join( os.path.dirname(os.path.realpath(__file__)), '..', @@ -41,13 +48,14 @@ class MqttBase: 'assets', '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 +69,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 +83,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/src/home/mqtt/_node.py b/src/home/mqtt/_node.py new file mode 100644 index 0000000..4e259a4 --- /dev/null +++ b/src/home/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/src/home/mqtt/_payload.py similarity index 99% rename from src/home/mqtt/payload/base_payload.py rename to src/home/mqtt/_payload.py index 1abd898..58eeae3 100644 --- a/src/home/mqtt/payload/base_payload.py +++ b/src/home/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/src/home/mqtt/_util.py b/src/home/mqtt/_util.py new file mode 100644 index 0000000..390d463 --- /dev/null +++ b/src/home/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/src/home/mqtt/_wrapper.py b/src/home/mqtt/_wrapper.py new file mode 100644 index 0000000..f858f88 --- /dev/null +++ b/src/home/mqtt/_wrapper.py @@ -0,0 +1,59 @@ +import paho.mqtt.client as mqtt + +from ._mqtt import Mqtt +from ._node import MqttNode +from ..config import config +from ..util import strgen + + +class MqttWrapper(Mqtt): + _nodes: list[MqttNode] + + 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 + for node in self._nodes: + 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/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/esp.py b/src/home/mqtt/module/diagnostics.py similarity index 54% rename from src/home/mqtt/payload/esp.py rename to src/home/mqtt/module/diagnostics.py index 171cdb9..5db5e99 100644 --- a/src/home/mqtt/payload/esp.py +++ b/src/home/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/src/home/mqtt/module/inverter.py b/src/home/mqtt/module/inverter.py new file mode 100644 index 0000000..d927a06 --- /dev/null +++ b/src/home/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 home.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/src/home/mqtt/module/ota.py b/src/home/mqtt/module/ota.py new file mode 100644 index 0000000..cd34332 --- /dev/null +++ b/src/home/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/src/home/mqtt/module/relay.py b/src/home/mqtt/module/relay.py new file mode 100644 index 0000000..e968031 --- /dev/null +++ b/src/home/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/src/home/mqtt/module/temphum.py b/src/home/mqtt/module/temphum.py new file mode 100644 index 0000000..fd02cca --- /dev/null +++ b/src/home/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/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/pio/products.py b/src/home/pio/products.py index 7649078..388da03 100644 --- a/src/home/pio/products.py +++ b/src/home/pio/products.py @@ -16,10 +16,6 @@ _products_dir = os.path.join( 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 diff --git a/src/home/telegram/_botcontext.py b/src/home/telegram/_botcontext.py index f343eeb..a143bfe 100644 --- a/src/home/telegram/_botcontext.py +++ b/src/home/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/bot.py b/src/home/telegram/bot.py index 10bfe06..7e22263 100644 --- a/src/home/telegram/bot.py +++ b/src/home/telegram/bot.py @@ -5,19 +5,19 @@ 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 @@ -33,26 +33,26 @@ from ._botcontext import Context 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 +_application: Optional[Application] = None _reporting: Optional[ReportingHelper] = None -_exception_handler: Optional[callable] = 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 +99,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 +107,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 +117,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 +139,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 +198,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 +252,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)) @@ -327,21 +327,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 @@ -411,7 +411,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 +419,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 +434,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,37 +477,38 @@ 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): @@ -535,7 +540,7 @@ def notify_all(text_getter: callable, continue text = text_getter(db.get_user_lang(user_id)) - _updater.bot.send_message(chat_id=user_id, + _application.bot.send_message(chat_id=user_id, text=text, parse_mode='HTML') @@ -543,33 +548,33 @@ def notify_all(text_getter: callable, 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, + _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) + _application.bot.send_photo(chat_id=user_id, **kwargs) def send_audio(user_id, **kwargs): - _updater.bot.send_audio(chat_id=user_id, **kwargs) + _application.bot.send_audio(chat_id=user_id, **kwargs) def send_file(user_id, **kwargs): - _updater.bot.send_document(chat_id=user_id, **kwargs) + _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, + _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) + _application.bot.delete_message(chat_id=user_id, message_id=message_id) def set_database(_db: BotDatabase): diff --git a/src/home/telegram/config.py b/src/home/telegram/config.py new file mode 100644 index 0000000..7a46087 --- /dev/null +++ b/src/home/telegram/config.py @@ -0,0 +1,75 @@ +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' + + @staticmethod + def schema() -> 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' + + @staticmethod + def schema() -> Optional[dict]: + return { + 'type': 'dict', + 'schema': { + 'id': {'type': 'string', 'required': True}, + 'token': {'type': 'string', 'required': True}, + } + } + + +class TelegramBotConfig(ConfigUnit, ABC): + @staticmethod + def schema() -> Optional[dict]: + return { + 'bot': { + 'type': 'dict', + 'schema': { + 'token': {'type': 'string', 'required': True}, + TelegramUserListType.USERS: {**TelegramBotConfig._userlist_schema(), 'required': True}, + TelegramUserListType.NOTIFY: TelegramBotConfig._userlist_schema(), + } + } + } + + @staticmethod + def _userlist_schema() -> dict: + return {'type': 'list', 'schema': {'type': ['string', 'int']}} + + @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]: + return list(map(_user_id_mapper, self['bot'][ult.value])) \ No newline at end of file diff --git a/src/home/temphum/__init__.py b/src/home/temphum/__init__.py index 55a7e1f..46d14e6 100644 --- a/src/home/temphum/__init__.py +++ b/src/home/temphum/__init__.py @@ -1,18 +1 @@ -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') +from .base import SensorType, BaseSensor diff --git a/src/home/temphum/base.py b/src/home/temphum/base.py index e774433..602cab7 100644 --- a/src/home/temphum/base.py +++ b/src/home/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 - def humidity(self) -> float: - pass - - @abstractmethod - def temperature(self) -> float: - pass - - -class I2CTempHumSensor(TempHumSensor, ABC): +class BaseSensor(ABC): def __init__(self, bus: int): super().__init__() self.bus = smbus.SMBus(bus) + def humidity(self) -> float: + pass + + def temperature(self) -> float: + pass + class SensorType(Enum): Si7021 = 'si7021' - DHT12 = 'dht12' + DHT12 = 'dht12' \ No newline at end of file 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/i2c.py b/src/home/temphum/i2c.py new file mode 100644 index 0000000..7d8e2e3 --- /dev/null +++ b/src/home/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/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/home/util.py b/src/home/util.py index 93a9d8f..35505bc 100644 --- a/src/home/util.py +++ b/src/home/util.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import socket import time @@ -6,17 +8,57 @@ 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 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 + + +class Addr: + host: str + port: int + + def __init__(self, host: str, port: int): + self.host = host + self.port = port + + @staticmethod + def fromstring(addr: str) -> Addr: + if addr.count(':') != 1: + raise ValueError('invalid host:port format') + + host, port = addr.split(':') + validate_ipv4_or_hostname(host, raise_exception=True) + + port = int(port) + if not 0 <= port <= 65535: + raise ValueError(f'invalid port {port}') + + return Addr(host, 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 +87,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 +220,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/src/inverter_bot.py b/src/inverter_bot.py index fd5acf3..ecf01fc 100755 --- a/src/inverter_bot.py +++ b/src/inverter_bot.py @@ -4,14 +4,16 @@ import re import datetime import json import itertools +import sys 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.config import config, AppConfigUnit from home.telegram import bot +from home.telegram.config import TelegramBotConfig, TelegramUserListType from home.inverter import ( wrapper_instance as inverter, beautify_table, @@ -24,12 +26,17 @@ from home.inverter.types import ( ACMode, OutputSourcePriority ) -from home.database.inverter_time_formats import * +from home.database.inverter_time_formats import FormatDate from home.api.types import BotType from home.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 +49,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' + + @staticmethod + def schema() -> 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( @@ -863,28 +917,27 @@ 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.enable_logging(BotType.INVERTER) - 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/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/inverter_mqtt_util.py b/src/inverter_mqtt_util.py new file mode 100755 index 0000000..791bf80 --- /dev/null +++ b/src/inverter_mqtt_util.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +from argparse import ArgumentParser +from home.config import config, app_config +from home.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(app_config['poll_freq']) + module_kwargs['generation_poll_freq'] = int(app_config['generation_poll_freq']) + node.load_module('inverter', **module_kwargs) + mqtt.add_node(node) + + mqtt.connect_and_loop() diff --git a/src/ipcam_server.py b/src/ipcam_server.py index 2c4915d..a54cd35 100755 --- a/src/ipcam_server.py +++ b/src/ipcam_server.py @@ -556,7 +556,7 @@ logger = logging.getLogger(__name__) # -------------------- if __name__ == '__main__': - config.load('ipcam_server') + config.load_app('ipcam_server') open_database() diff --git a/src/mqtt_node_util.py b/src/mqtt_node_util.py new file mode 100755 index 0000000..ce954ae --- /dev/null +++ b/src/mqtt_node_util.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import os.path + +from time import sleep +from typing import Optional +from argparse import ArgumentParser, ArgumentError + +from home.config import config +from home.mqtt import MqttNode, MqttWrapper, get_mqtt_modules +from home.mqtt import MqttNodesConfig + +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('--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: + module_instance = mqtt_node.load_module(m) + 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/src/openwrt_log_analyzer.py b/src/openwrt_log_analyzer.py index d31c3bf..35b755f 100755 --- a/src/openwrt_log_analyzer.py +++ b/src/openwrt_log_analyzer.py @@ -54,7 +54,7 @@ def main(mac: str, if __name__ == '__main__': - config.load('openwrt_log_analyzer') + config.load_app('openwrt_log_analyzer') for ap in config['openwrt_log_analyzer']['aps']: state_file = config['simple_state']['file'] state_file = state_file.replace('.txt', f'-{ap}.txt') diff --git a/src/openwrt_logger.py b/src/openwrt_logger.py index 3b19de2..97fe7a9 100755 --- a/src/openwrt_logger.py +++ b/src/openwrt_logger.py @@ -46,7 +46,7 @@ if __name__ == '__main__': parser.add_argument('--access-point', type=int, required=True, help='access point number') - arg = config.load('openwrt_logger', parser=parser) + arg = config.load_app('openwrt_logger', parser=parser) state = SimpleState(file=config['simple_state']['file'].replace('{ap}', str(arg.access_point)), default={'seek': 0, 'size': 0}) diff --git a/src/pio_ini.py b/src/pio_ini.py index 19dd707..920c3e5 100755 --- a/src/pio_ini.py +++ b/src/pio_ini.py @@ -54,12 +54,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 +81,9 @@ 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': + defines[f'CONFIG_{define_name}'] = True + return defines[f'CONFIG_{define_name}'] = str(attr_value) bsd_walk(product_config, f) return defines, enums diff --git a/src/polaris_kettle_bot.py b/src/polaris_kettle_bot.py index 088707d..80baef3 100755 --- a/src/polaris_kettle_bot.py +++ b/src/polaris_kettle_bot.py @@ -10,7 +10,7 @@ import paho.mqtt.client as mqtt from home.telegram import bot from home.api.types import BotType -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.config import config from home.util import chunks from syncleo import ( @@ -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__) diff --git a/src/polaris_kettle_util.py b/src/polaris_kettle_util.py index 81326dd..12c4388 100755 --- a/src/polaris_kettle_util.py +++ b/src/polaris_kettle_util.py @@ -8,7 +8,7 @@ import paho.mqtt.client as mqtt from typing import Optional from argparse import ArgumentParser from queue import SimpleQueue -from home.mqtt import MqttBase +from home.mqtt import Mqtt from home.config import config from syncleo import ( Kettle, @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) control_tasks = SimpleQueue() -class MqttServer(MqttBase): +class MqttServer(Mqtt): def __init__(self): super().__init__(clean_session=False) @@ -75,7 +75,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/src/pump_bot.py b/src/pump_bot.py index de925db..25f06fd 100755 --- a/src/pump_bot.py +++ b/src/pump_bot.py @@ -2,14 +2,34 @@ from enum import Enum from typing import Optional from telegram import ReplyKeyboardMarkup, User +from time import time +from datetime import datetime -from home.config import config +from home.config import config, is_development_mode from home.telegram import bot from home.telegram._botutil import user_any_name from home.relay.sunxi_h3_client import RelayClient from home.api.types import BotType +from home.mqtt import MqttNode, MqttWrapper, MqttPayload +from home.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule +from home.mqtt.module.temphum import MqttTemphumDataPayload +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('pump_bot') + +config.load_app('pump_bot') + +mqtt: Optional[MqttWrapper] = None +mqtt_node: Optional[MqttNode] = None +mqtt_relay_module: Optional[MqttRelayModule] = None +time_format = '%d.%m.%Y, %H:%M:%S' + +watering_mcu_status = { + 'last_time': 0, + 'last_boot_time': 0, + 'relay_opened': False, + 'ambient_temp': 0.0, + 'ambient_rh': 0.0, +} bot.initialize() bot.lang.ru( @@ -18,17 +38,27 @@ bot.lang.ru( enable="Включить", enable_silently="Включить тихо", - enabled="Включен ✅", + enabled="Насос включен ✅", disable="Выключить", disable_silently="Выключить тихо", - disabled="Выключен ❌", + disabled="Насос выключен ❌", + + start_watering="Включить полив", + stop_watering="Отключить полив", + + status="Статус насоса", + watering_status="Статус полива", - status="Статус", done="Готово 👌", + sent="Команда отправлена", + user_action_notification='Пользователь %s %s насос.', + user_watering_notification='Пользователь %s %s полив.', user_action_on="включил", user_action_off="выключил", + user_action_watering_on="включил", + user_action_watering_off="выключил", ) bot.lang.en( start_message="Select command on the keyboard", @@ -36,23 +66,35 @@ bot.lang.en( enable="Turn ON", enable_silently="Turn ON silently", - enabled="Turned ON ✅", + enabled="The pump is turned ON ✅", disable="Turn OFF", disable_silently="Turn OFF silently", - disabled="Turned OFF ❌", + disabled="The pump is turned OFF ❌", + + start_watering="Start watering", + stop_watering="Stop watering", + + status="Pump status", + watering_status="Watering status", - status="Status", done="Done 👌", + sent="Request sent", + user_action_notification='User %s turned the pump %s.', + user_watering_notification='User %s %s 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: @@ -75,11 +117,24 @@ def off(ctx: bot.Context, silent=False) -> None: notify(ctx.user, UserAction.OFF) +def watering_on(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(True, config.get('mqtt_water_relay.secret')) + ctx.reply(ctx.lang('sent')) + notify(ctx.user, UserAction.WATERING_ON) + + +def watering_off(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(False, config.get('mqtt_water_relay.secret')) + ctx.reply(ctx.lang('sent')) + notify(ctx.user, UserAction.WATERING_OFF) + + 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('user_action_notification', lang, + return 'ℹ ' + bot.lang.get(notification_key, lang, user.id, user_name, action_name) bot.notify_all(text_getter, exclude=(user.id,)) @@ -100,6 +155,16 @@ def disable_handler(ctx: bot.Context) -> None: off(ctx) +@bot.handler(message='start_watering') +def start_watering(ctx: bot.Context) -> None: + watering_on(ctx) + + +@bot.handler(message='stop_watering') +def stop_watering(ctx: bot.Context) -> None: + watering_off(ctx) + + @bot.handler(message='disable_silently') def disable_s_handler(ctx: bot.Context) -> None: off(ctx, True) @@ -112,20 +177,79 @@ def status(ctx: bot.Context) -> None: ) +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') +def watering_status(ctx: bot.Context) -> None: + buf = '' + if 0 < watering_mcu_status["last_time"] < time()-1800: + buf += 'WARNING! long time no reports from mcu! maybe something\'s wrong\n' + buf += f'last report time: {_get_timestamp_as_string(watering_mcu_status["last_time"])}\n' + if watering_mcu_status["last_boot_time"] != 0: + buf += f'boot time: {_get_timestamp_as_string(watering_mcu_status["last_boot_time"])}\n' + buf += 'relay opened: ' + ('yes' if watering_mcu_status['relay_opened'] else 'no') + '\n' + buf += f'ambient temp & humidity: {watering_mcu_status["ambient_temp"]} °C, {watering_mcu_status["ambient_rh"]}%' + ctx.reply(buf) + + @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: - buttons = [ - [ctx.lang('enable'), ctx.lang('disable')], - ] - + buttons = [] if ctx.user_id in config['bot']['silent_users']: buttons.append([ctx.lang('enable_silently'), ctx.lang('disable_silently')]) - - buttons.append([ctx.lang('status')]) + 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 + + if __name__ == '__main__': + mqtt = MqttWrapper() + mqtt_node = MqttNode(node_id=config.get('mqtt_water_relay.node_id')) + if is_development_mode(): + mqtt_node.load_module('diagnostics') + + mqtt_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.enable_logging(BotType.PUMP) bot.run() + + try: + mqtt.disconnect() + except: + pass diff --git a/src/pump_mqtt_bot.py b/src/pump_mqtt_bot.py index d3b6de4..4036d3a 100755 --- a/src/pump_mqtt_bot.py +++ b/src/pump_mqtt_bot.py @@ -8,13 +8,12 @@ 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 home.mqtt import MqttNode, MqttPayload +from home.mqtt.module.relay import MqttRelayState +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('pump_mqtt_bot') +config.load_app('pump_mqtt_bot') bot.initialize() bot.lang.ru( @@ -70,7 +69,7 @@ bot.lang.en( ) -mqtt_relay: Optional[MqttRelay] = None +mqtt: Optional[MqttNode] = None relay_state = MqttRelayState() @@ -99,14 +98,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 +156,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/src/relay_mqtt_bot.py b/src/relay_mqtt_bot.py index ebbff82..9de8c7e 100755 --- a/src/relay_mqtt_bot.py +++ b/src/relay_mqtt_bot.py @@ -1,18 +1,62 @@ #!/usr/bin/env python3 +import sys + from enum import Enum -from typing import Optional +from typing import Optional, Union from telegram import ReplyKeyboardMarkup from functools import partial -from home.config import config +from home.config import config, AppConfigUnit, Translation 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 +from home.telegram.config import TelegramBotConfig +from home.mqtt import MqttPayload, MqttNode, MqttWrapper, MqttModule +from home.mqtt import MqttNodesConfig +from home.mqtt.module.relay import MqttRelayModule, MqttRelayState +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('relay_mqtt_bot') +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') + + @staticmethod + def schema() -> 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( @@ -34,7 +78,10 @@ status_emoji = { 'on': '✅', 'off': '❌' } -mqtt_relay: Optional[MqttRelay] = None + + +mqtt: MqttWrapper +relay_nodes: dict[str, Union[MqttRelayModule, MqttModule]] = {} relay_states: dict[str, MqttRelayState] = {} @@ -43,70 +90,75 @@ class UserAction(Enum): OFF = 'off' -def on_mqtt_message(home_id, message: MqttPayload): +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 home_id not in relay_states: - relay_states[home_id] = MqttRelayState() - relay_states[home_id].update(**kwargs) + if node.id not in relay_states: + relay_states[node.id] = MqttRelayState() + relay_states[node.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')) +async def enable_handler(node_id: str, ctx: bot.Context) -> None: + relay_nodes[node_id].switchpower(True) + await 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')) +async def disable_handler(node_id: str, ctx: bot.Context) -> None: + relay_nodes[node_id].switchpower(False) + await ctx.reply(ctx.lang('done')) -def start(ctx: bot.Context) -> None: - ctx.reply(ctx.lang('start_message')) +async def start(ctx: bot.Context) -> None: + await ctx.reply(ctx.lang('start_message')) @bot.exceptionhandler -def exception_handler(e: Exception, ctx: bot.Context) -> bool: +async def exception_handler(e: Exception, ctx: bot.Context) -> bool: return False @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: buttons = [] - for device_id, data in config['relays'].items(): - labels = data['labels'] - type_emoji = type_emojis[data['type']] - row = [f'{type_emoji}{status_emoji[i.value]} {labels[ctx.user_lang]}' + for node_id in config.app_config['relay_nodes']: + node_data = mqtt_nodes_config.get_node(node_id) + type_emoji = type_emojis[node_data['relay']['device_type']] + row = [f'{type_emoji}{status_emoji[i.value]} {config.app_config.get_relay_name_translated(ctx.user_lang, node_id)}' for i in UserAction] buttons.append(row) return ReplyKeyboardMarkup(buttons, one_time_keyboard=False) -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']}) +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[data['type']] + type_emoji = type_emojis[node_data['relay']['device_type']] - for action in UserAction: - messages = [] - for _lang, _label in labels.items(): - messages.append(f'{type_emoji}{status_emoji[action.value]} {labels[_lang]}') - bot.handler(texts=messages)(partial(enable_handler if action == UserAction.ON else disable_handler, device_id)) + for 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_relay = MqttRelay(devices=devices) - mqtt_relay.set_message_callback(on_mqtt_message) - mqtt_relay.configure_tls() - mqtt_relay.connect_and_loop(loop_forever=False) +mqtt.connect_and_loop(loop_forever=False) - # bot.enable_logging(BotType.RELAY_MQTT) - bot.run(start_handler=start) +bot.run(start_handler=start) - mqtt_relay.disconnect() +mqtt.disconnect() diff --git a/src/relay_mqtt_http_proxy.py b/src/relay_mqtt_http_proxy.py index 098facc..2bc2c4a 100755 --- a/src/relay_mqtt_http_proxy.py +++ b/src/relay_mqtt_http_proxy.py @@ -1,17 +1,19 @@ #!/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 +from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule +from home.mqtt.module.relay import MqttRelayState, MqttRelayModule +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from typing import Optional, Union -mqtt_relay: Optional[MqttRelay] = None +mqtt: Optional[MqttWrapper] = None +mqtt_nodes: dict[str, MqttNode] = {} +relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {} relay_states: dict[str, MqttRelayState] = {} -def on_mqtt_message(device_id, message: MqttPayload): +def on_mqtt_message(node: MqttNode, + message: MqttPayload): if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): kwargs = dict(rssi=message.rssi, enabled=message.flags.state) if device_id not in relay_states: @@ -29,17 +31,22 @@ class RelayMqttHttpProxy(http.HTTPServer): async def _relay_on_off(self, enable: Optional[bool], req: http.Request): - device_id = req.match_info['id'] - device_secret = req.query['secret'] + 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 device_id in relay_states and relay_states[device_id].ever_updated: - cur_state = relay_states[device_id].enabled + 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 - mqtt_relay.set_power(device_id, enable, device_secret) + if not node.secret: + node.secret = node_secret + relay_module.switchpower(enable) return self.ok() async def relay_on(self, req: http.Request): @@ -53,15 +60,21 @@ class RelayMqttHttpProxy(http.HTTPServer): if __name__ == '__main__': - config.load('relay_mqtt_http_proxy') + config.load_app('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) + mqtt = MqttWrapper() + for device_id, data in config['relays'].items(): + mqtt_node = MqttNode(node_id=device_id) + relay_modules[device_id] = mqtt_node.load_module('relay') + mqtt_nodes[device_id] = mqtt_node + mqtt_node.add_payload_callback(on_mqtt_message) + mqtt.add_node(mqtt_node) + mqtt_node.add_payload_callback(on_mqtt_message) + + mqtt.connect_and_loop(loop_forever=False) proxy = RelayMqttHttpProxy(config.get_addr('server.listen')) try: proxy.run() except KeyboardInterrupt: - mqtt_relay.disconnect() + mqtt.disconnect() diff --git a/src/sensors_bot.py b/src/sensors_bot.py index dc081b0..152dd24 100755 --- a/src/sensors_bot.py +++ b/src/sensors_bot.py @@ -23,7 +23,7 @@ from home.api.types import ( TemperatureSensorLocation ) -config.load('sensors_bot') +config.load_app('sensors_bot') bot.initialize() bot.lang.ru( 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/src/sound_bot.py b/src/sound_bot.py index 186337a..32371bd 100755 --- a/src/sound_bot.py +++ b/src/sound_bot.py @@ -14,7 +14,7 @@ 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 home.util import Addr, chunks, filesize_fmt from home.telegram import bot @@ -23,11 +23,11 @@ from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardBu 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 +142,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 +188,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 diff --git a/src/sound_node.py b/src/sound_node.py index 9d53362..b0b4a67 100755 --- a/src/sound_node.py +++ b/src/sound_node.py @@ -77,7 +77,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/src/sound_sensor_node.py index d9a8999..404fdf4 100755 --- a/src/sound_sensor_node.py +++ b/src/sound_sensor_node.py @@ -4,7 +4,7 @@ import os import sys from home.config import config -from home.util import parse_addr +from home.util import Addr from home.soundsensor import SoundSensorNode logger = logging.getLogger(__name__) @@ -14,14 +14,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/src/sound_sensor_server.py index aa62608..b660210 100755 --- a/src/sound_sensor_server.py +++ b/src/sound_sensor_server.py @@ -6,7 +6,7 @@ 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.util import Addr from home.api import WebAPIClient, RequestParams from home.api.types import SoundSensorLocation from home.soundsensor import SoundSensorServer, SoundSensorHitHandler @@ -159,7 +159,7 @@ 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)) @@ -172,12 +172,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/src/ssh_tunnels_config_util.py index 3b2ba6e..963c01b 100755 --- a/src/ssh_tunnels_config_util.py +++ b/src/ssh_tunnels_config_util.py @@ -3,12 +3,12 @@ from home.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/src/temphum_mqtt_node.py b/src/temphum_mqtt_node.py new file mode 100755 index 0000000..c3d1975 --- /dev/null +++ b/src/temphum_mqtt_node.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import asyncio +import json +import logging + +from typing import Optional + +from home.config import config +from home.temphum import SensorType, BaseSensor +from home.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/src/temphum_mqtt_receiver.py similarity index 70% rename from src/sensors_mqtt_receiver.py rename to src/temphum_mqtt_receiver.py index a377ddd..2b30800 100755 --- a/src/sensors_mqtt_receiver.py +++ b/src/temphum_mqtt_receiver.py @@ -2,21 +2,11 @@ import paho.mqtt.client as mqtt import re -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 home.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 +37,11 @@ class MqttServer(MqttBase): if __name__ == '__main__': - config.load('sensors_mqtt_receiver') + config.load_app('temphum_mqtt_receiver') - server = MqttServer() - server.connect_and_loop() + mqtt = MqttWrapper(clean_session=False) + node = MqttNode(node_id='+') + node.load_module('temphum', write_to_database=True) + mqtt.add_node(node) + + mqtt.connect_and_loop() \ No newline at end of file diff --git a/src/temphum_smbus_util.py b/src/temphum_smbus_util.py index 0f90835..c06bacd 100755 --- a/src/temphum_smbus_util.py +++ b/src/temphum_smbus_util.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from argparse import ArgumentParser -from home.temphum import SensorType, create_sensor +from home.temphum import SensorType +from home.temphum.i2c import create_sensor if __name__ == '__main__': diff --git a/src/temphumd.py b/src/temphumd.py index f4d1fca..c3d1975 100755 --- a/src/temphumd.py +++ b/src/temphumd.py @@ -6,10 +6,11 @@ import logging from typing import Optional from home.config import config -from home.temphum import SensorType, create_sensor, TempHumSensor +from home.temphum import SensorType, BaseSensor +from home.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 +63,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/test_new_config.py b/src/test_new_config.py new file mode 100755 index 0000000..db9eae3 --- /dev/null +++ b/src/test_new_config.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +from home.config import config +from home.mqtt import MqttNodesConfig +from home.telegram.config import TelegramUserIdsConfig +from pprint import pprint + + +if __name__ == '__main__': + config.load_app(name=False) + + c = TelegramUserIdsConfig() + pprint(c.get()) \ No newline at end of file diff --git a/src/web_api.py b/src/web_api.py index 0ddc6bd..0aa994a 100755 --- a/src/web_api.py +++ b/src/web_api.py @@ -231,7 +231,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/systemd/inverter_mqtt_receiver.service b/systemd/inverter_mqtt_receiver.service new file mode 100644 index 0000000..fedf11f --- /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/src/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..34272bb 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/src/inverter_mqtt_util.py sender WorkingDirectory=/home/user [Install] diff --git a/systemd/ipcam_rtsp2hls@.service b/systemd/ipcam_rtsp2hls@.service index addd819..efcdd6a 100644 --- a/systemd/ipcam_rtsp2hls@.service +++ b/systemd/ipcam_rtsp2hls@.service @@ -9,6 +9,8 @@ User=user Group=user EnvironmentFile=/etc/ipcam_rtsp2hls.conf.d/%i.conf ExecStart=/home/user/homekit/tools/ipcam_rtsp2hls.sh --name %i --user $USER --password $PASSWORD --ip $IP --port $PORT $ARGS +Restart=on-failure +RestartSec=3 [Install] WantedBy=multi-user.target diff --git a/systemd/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/test/mqtt_relay_server_util.py b/test/mqtt_relay_server_util.py new file mode 100755 index 0000000..ac6a9ae --- /dev/null +++ b/test/mqtt_relay_server_util.py @@ -0,0 +1,17 @@ +#!/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 src.home.config import config +from src.home.mqtt.relay import MQTTRelayClient + + +if __name__ == '__main__': + 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..0d8c764 --- /dev/null +++ b/test/mqtt_relay_util.py @@ -0,0 +1,38 @@ +#!/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 argparse import ArgumentParser +from src.home.config import config +from src.home.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_amixer.py b/test/test_amixer.py index c8bd546..464941e 100755 --- a/test/test_amixer.py +++ b/test/test_amixer.py @@ -28,7 +28,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..e80eb4c 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -13,7 +13,7 @@ from src.home.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")) diff --git a/test/test_esp32_cam.py b/test/test_esp32_cam.py index 27ce379..6a4ad25 100755 --- a/test/test_esp32_cam.py +++ b/test/test_esp32_cam.py @@ -10,7 +10,7 @@ sys.path.extend([ from pprint import pprint from argparse import ArgumentParser from time import sleep -from src.home.util import parse_addr +from src.home.util import Addr from src.home.camera import esp32 from src.home.config import config @@ -21,8 +21,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..621c0e9 100755 --- a/test/test_inverter_monitor.py +++ b/test/test_inverter_monitor.py @@ -372,5 +372,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..5f313a4 100644 --- a/test/test_ipcam_server_cleanup.py +++ b/test/test_ipcam_server_cleanup.py @@ -77,5 +77,5 @@ def cleanup_job(): if __name__ == '__main__': - config.load('ipcam_server') + config.load_app('ipcam_server') cleanup_job() diff --git a/test/test_record_upload.py b/test/test_record_upload.py index cbd3ca2..835504f 100755 --- a/test/test_record_upload.py +++ b/test/test_record_upload.py @@ -13,7 +13,7 @@ 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 src.home.util import Addr logger = logging.getLogger(__name__) @@ -64,11 +64,11 @@ 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, diff --git a/test/test_send_fake_sound_hit.py b/test/test_send_fake_sound_hit.py index 9660c45..61886cd 100755 --- a/test/test_send_fake_sound_hit.py +++ b/test/test_send_fake_sound_hit.py @@ -8,7 +8,7 @@ sys.path.extend([ ]) from argparse import ArgumentParser -from src.home.util import send_datagram, stringify, parse_addr +from src.home.util import send_datagram, stringify, Addr if __name__ == '__main__': @@ -22,4 +22,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_sound_server_api.py b/test/test_sound_server_api.py index e68c6f8..5295a5d 100755 --- a/test/test_sound_server_api.py +++ b/test/test_sound_server_api.py @@ -56,7 +56,7 @@ def hits_sender(): if __name__ == '__main__': - config.load('test_api') + config.load_app('test_api') hc = HitCounter() api = WebAPIClient() diff --git a/test/test_telegram_aio_send_photo.py b/test/test_telegram_aio_send_photo.py index 705e534..4d05c03 100644 --- a/test/test_telegram_aio_send_photo.py +++ b/test/test_telegram_aio_send_photo.py @@ -20,7 +20,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/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 From 327a5298359027099631c3c9967b7585928cd367 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 21:54:56 +0300 Subject: [PATCH 02/51] port relay_mqtt_http_proxy to new config scheme; config: support addr types & normalization --- src/home/config/_configs.py | 8 ++-- src/home/config/config.py | 61 ++++++++++++++++--------- src/home/inverter/config.py | 4 +- src/home/mqtt/_config.py | 8 ++-- src/home/mqtt/_wrapper.py | 5 ++- src/home/telegram/config.py | 12 ++--- src/home/util.py | 33 ++++++++++---- src/inverter_bot.py | 4 +- src/relay_mqtt_bot.py | 4 +- src/relay_mqtt_http_proxy.py | 87 +++++++++++++++++++++++++++++------- src/test_new_config.py | 12 ----- 11 files changed, 158 insertions(+), 80 deletions(-) delete mode 100755 src/test_new_config.py diff --git a/src/home/config/_configs.py b/src/home/config/_configs.py index 3a1aae5..1628cba 100644 --- a/src/home/config/_configs.py +++ b/src/home/config/_configs.py @@ -5,8 +5,8 @@ from typing import Optional class ServicesListConfig(ConfigUnit): NAME = 'services_list' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'list', 'empty': False, @@ -19,8 +19,8 @@ class ServicesListConfig(ConfigUnit): class LinuxBoardsConfig(ConfigUnit): NAME = 'linux_boards' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'dict', 'schema': { diff --git a/src/home/config/config.py b/src/home/config/config.py index aef9ee7..dc00d2e 100644 --- a/src/home/config/config.py +++ b/src/home/config/config.py @@ -1,10 +1,10 @@ import yaml import logging import os -import pprint +import cerberus +import cerberus.errors from abc import ABC -from cerberus import Validator, DocumentError from typing import Optional, Any, MutableMapping, Union from argparse import ArgumentParser from enum import Enum, auto @@ -12,11 +12,20 @@ from os.path import join, isdir, isfile from ..util import Addr +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() @@ -95,10 +104,19 @@ class ConfigUnit(BaseConfigUnit): raise IOError(f'\'{name}.yaml\' not found') - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return None + @classmethod + def _addr_schema(cls, required=False, **kwargs): + return { + 'type': 'addr', + 'coerce': Addr.fromstring, + 'required': required, + **kwargs + } + def validate(self): schema = self.schema() if not schema: @@ -109,7 +127,7 @@ class ConfigUnit(BaseConfigUnit): schema['logging'] = { 'type': 'dict', 'schema': { - 'logging': {'type': 'bool'} + 'logging': {'type': 'boolean'} } } @@ -125,27 +143,27 @@ class ConfigUnit(BaseConfigUnit): except KeyError: pass + v = MyValidator() + if rst == RootSchemaType.DICT: - v = Validator({'document': { - 'type': 'dict', - 'keysrules': {'type': 'string'}, - 'valuesrules': schema - }}) - result = v.validate({'document': self._data}) + normalized = v.validated({'document': self._data}, + {'document': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': schema + }})['document'] elif rst == RootSchemaType.LIST: - v = Validator({'document': schema}) - result = v.validate({'document': self._data}) + v = MyValidator() + normalized = v.validated({'document': self._data}, {'document': schema})['document'] else: - v = Validator(schema) - result = v.validate(self._data) - # pprint.pprint(self._data) - if not result: - # pprint.pprint(v.errors) - raise DocumentError(f'{self.__class__.__name__}: failed to validate data:\n{pprint.pformat(v.errors)}') + normalized = v.validated(self._data, schema) + + self._data = normalized + try: self.custom_validator(self._data) except Exception as e: - raise DocumentError(f'{self.__class__.__name__}: {str(e)}') + raise cerberus.DocumentError(f'{self.__class__.__name__}: {str(e)}') @staticmethod def custom_validator(data): @@ -238,7 +256,7 @@ class Config: no_config=False): global app_config - if issubclass(name, AppConfigUnit) or name == AppConfigUnit: + if not isinstance(name, str) and not isinstance(name, bool) and issubclass(name, AppConfigUnit) or name == AppConfigUnit: self.app_name = name.NAME self.app_config = name() app_config = self.app_config @@ -278,6 +296,7 @@ class Config: 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(), diff --git a/src/home/inverter/config.py b/src/home/inverter/config.py index 62b8859..e284dfe 100644 --- a/src/home/inverter/config.py +++ b/src/home/inverter/config.py @@ -5,8 +5,8 @@ from typing import Optional class InverterdConfig(ConfigUnit): NAME = 'inverterd' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'remote_addr': {'type': 'string'}, 'local_addr': {'type': 'string'}, diff --git a/src/home/mqtt/_config.py b/src/home/mqtt/_config.py index f9047b4..9ba9443 100644 --- a/src/home/mqtt/_config.py +++ b/src/home/mqtt/_config.py @@ -9,8 +9,8 @@ MqttCreds = namedtuple('MqttCreds', 'username, password') class MqttConfig(ConfigUnit): NAME = 'mqtt' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: addr_schema = { 'type': 'dict', 'required': True, @@ -64,8 +64,8 @@ class MqttConfig(ConfigUnit): class MqttNodesConfig(ConfigUnit): NAME = 'mqtt_nodes' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'common': { 'type': 'dict', diff --git a/src/home/mqtt/_wrapper.py b/src/home/mqtt/_wrapper.py index f858f88..3c2774c 100644 --- a/src/home/mqtt/_wrapper.py +++ b/src/home/mqtt/_wrapper.py @@ -2,7 +2,6 @@ import paho.mqtt.client as mqtt from ._mqtt import Mqtt from ._node import MqttNode -from ..config import config from ..util import strgen @@ -34,8 +33,10 @@ class MqttWrapper(Mqtt): def on_message(self, client: mqtt.Client, userdata, msg): try: topic = msg.topic + topic_node = topic[len(self._topic_prefix)+1:topic.find('/', len(self._topic_prefix)+1)] for node in self._nodes: - node.on_message(topic[len(f'{self._topic_prefix}/{node.id}/'):], msg.payload) + if node.id in ('+', topic_node): + node.on_message(topic[len(f'{self._topic_prefix}/{node.id}/'):], msg.payload) except Exception as e: self._logger.exception(str(e)) diff --git a/src/home/telegram/config.py b/src/home/telegram/config.py index 7a46087..4c7d74b 100644 --- a/src/home/telegram/config.py +++ b/src/home/telegram/config.py @@ -12,8 +12,8 @@ class TelegramUserListType(Enum): class TelegramUserIdsConfig(ConfigUnit): NAME = 'telegram_user_ids' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'roottype': 'dict', 'type': 'integer' @@ -32,8 +32,8 @@ def _user_id_mapper(user: Union[str, int]) -> int: class TelegramChatsConfig(ConfigUnit): NAME = 'telegram_chats' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'type': 'dict', 'schema': { @@ -44,8 +44,8 @@ class TelegramChatsConfig(ConfigUnit): class TelegramBotConfig(ConfigUnit, ABC): - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { 'bot': { 'type': 'dict', diff --git a/src/home/util.py b/src/home/util.py index 35505bc..1e12243 100644 --- a/src/home/util.py +++ b/src/home/util.py @@ -12,7 +12,7 @@ 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 logger = logging.getLogger(__name__) @@ -38,26 +38,43 @@ def validate_ipv4_or_hostname(address: str, raise_exception: bool = False) -> bo class Addr: host: str - port: int + port: Optional[int] - def __init__(self, host: str, port: int): + def __init__(self, host: str, port: Optional[int] = None): self.host = host self.port = port @staticmethod def fromstring(addr: str) -> Addr: - if addr.count(':') != 1: + colons = addr.count(':') + if colons != 1: raise ValueError('invalid host:port format') - host, port = addr.split(':') + if not colons: + host = addr + port= None + else: + host, port = addr.split(':') + validate_ipv4_or_hostname(host, raise_exception=True) - port = int(port) - if not 0 <= port <= 65535: - raise ValueError(f'invalid port {port}') + 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): diff --git a/src/inverter_bot.py b/src/inverter_bot.py index ecf01fc..d35e606 100755 --- a/src/inverter_bot.py +++ b/src/inverter_bot.py @@ -55,8 +55,8 @@ logger = logging.getLogger(__name__) class InverterBotConfig(AppConfigUnit, TelegramBotConfig): NAME = 'inverter_bot' - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: acmode_item_schema = { 'thresholds': { 'type': 'list', diff --git a/src/relay_mqtt_bot.py b/src/relay_mqtt_bot.py index 9de8c7e..020dc08 100755 --- a/src/relay_mqtt_bot.py +++ b/src/relay_mqtt_bot.py @@ -32,8 +32,8 @@ class RelayMqttBotConfig(AppConfigUnit, TelegramBotConfig): super().__init__() self._strings = Translation('mqtt_nodes') - @staticmethod - def schema() -> Optional[dict]: + @classmethod + def schema(cls) -> Optional[dict]: return { **super(TelegramBotConfig).schema(), 'relay_nodes': { diff --git a/src/relay_mqtt_http_proxy.py b/src/relay_mqtt_http_proxy.py index 2bc2c4a..e13c04a 100755 --- a/src/relay_mqtt_http_proxy.py +++ b/src/relay_mqtt_http_proxy.py @@ -1,24 +1,69 @@ #!/usr/bin/env python3 +import logging + from home import http -from home.config import config -from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule -from home.mqtt.module.relay import MqttRelayState, MqttRelayModule +from home.config import config, AppConfigUnit +from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig +from home.mqtt.module.relay import MqttRelayState, MqttRelayModule, MqttPowerStatusPayload from home.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 = 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) + 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): @@ -44,8 +89,7 @@ class RelayMqttHttpProxy(http.HTTPServer): cur_state = False enable = not cur_state - if not node.secret: - node.secret = node_secret + node.secret = node_secret relay_module.switchpower(enable) return self.ok() @@ -60,20 +104,29 @@ class RelayMqttHttpProxy(http.HTTPServer): if __name__ == '__main__': - config.load_app('relay_mqtt_http_proxy') + config.load_app(RelayMqttHttpProxyConfig) - mqtt = MqttWrapper() - for device_id, data in config['relays'].items(): - mqtt_node = MqttNode(node_id=device_id) - relay_modules[device_id] = mqtt_node.load_module('relay') - mqtt_nodes[device_id] = mqtt_node + mqtt = 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_node.add_payload_callback(on_mqtt_message) + mqtt_nodes[node_id] = mqtt_node mqtt.connect_and_loop(loop_forever=False) - proxy = RelayMqttHttpProxy(config.get_addr('server.listen')) + proxy = RelayMqttHttpProxy(config.app_config['listen_addr']) try: proxy.run() except KeyboardInterrupt: diff --git a/src/test_new_config.py b/src/test_new_config.py deleted file mode 100755 index db9eae3..0000000 --- a/src/test_new_config.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -from home.config import config -from home.mqtt import MqttNodesConfig -from home.telegram.config import TelegramUserIdsConfig -from pprint import pprint - - -if __name__ == '__main__': - config.load_app(name=False) - - c = TelegramUserIdsConfig() - pprint(c.get()) \ No newline at end of file From 2631c58961c2f5ec90be560a8f5152fe27339a90 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 22:11:41 +0300 Subject: [PATCH 03/51] fix mqtt_node_util --- src/electricity_calc.py | 1 - src/home/config/config.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/electricity_calc.py b/src/electricity_calc.py index c3cb233..8ea5a1c 100755 --- a/src/electricity_calc.py +++ b/src/electricity_calc.py @@ -3,7 +3,6 @@ import logging import os import sys import inspect -import zoneinfo from home.config import config # do not remove this import! from datetime import datetime, timedelta diff --git a/src/home/config/config.py b/src/home/config/config.py index dc00d2e..7344386 100644 --- a/src/home/config/config.py +++ b/src/home/config/config.py @@ -256,7 +256,10 @@ class Config: no_config=False): global app_config - if not isinstance(name, str) and not isinstance(name, bool) and issubclass(name, AppConfigUnit) or name == AppConfigUnit: + if not no_config \ + and not isinstance(name, str) \ + and not isinstance(name, bool) \ + and issubclass(name, AppConfigUnit) or name == AppConfigUnit: self.app_name = name.NAME self.app_config = name() app_config = self.app_config From 3790c2205396cf860738f297e6ddc49cd2b2a03f Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 22:29:24 +0300 Subject: [PATCH 04/51] new config: port openwrt_logger and webapiclient --- doc/openwrt_logger.md | 28 +++++++++++++++++++++++ src/home/api/__init__.py | 12 ++++++++-- src/home/api/__init__.pyi | 3 ++- src/home/api/config.py | 15 +++++++++++++ src/home/api/web_api_client.py | 32 +++++++++++++------------- src/home/database/_base.py | 9 ++++++++ src/home/database/simple_state.py | 14 +++++++----- src/home/database/sqlite.py | 6 ++--- src/home/telegram/_botutil.py | 2 +- src/home/telegram/bot.py | 4 ++-- src/inverter_bot.py | 4 ++-- src/openwrt_log_analyzer.py | 2 +- src/openwrt_logger.py | 37 +++++++++++-------------------- src/sensors_bot.py | 4 ++-- src/sound_bot.py | 6 ++--- src/sound_sensor_server.py | 6 ++--- test/test_api.py | 4 ++-- test/test_record_upload.py | 4 ++-- test/test_sound_server_api.py | 4 ++-- 19 files changed, 124 insertions(+), 72 deletions(-) create mode 100644 doc/openwrt_logger.md create mode 100644 src/home/api/config.py create mode 100644 src/home/database/_base.py 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/src/home/api/__init__.py b/src/home/api/__init__.py index 782a61e..d641f62 100644 --- a/src/home/api/__init__.py +++ b/src/home/api/__init__.py @@ -1,11 +1,19 @@ import importlib -__all__ = ['WebAPIClient', 'RequestParams'] +__all__ = [ + # web_api_client.py + 'WebApiClient', + 'RequestParams', + + # config.py + 'WebApiConfig' +] def __getattr__(name): if name in __all__: - module = importlib.import_module(f'.web_api_client', __name__) + 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/src/home/api/__init__.pyi b/src/home/api/__init__.pyi index 1b812d6..5b98161 100644 --- a/src/home/api/__init__.pyi +++ b/src/home/api/__init__.pyi @@ -1,4 +1,5 @@ from .web_api_client import ( RequestParams as RequestParams, - WebAPIClient as WebAPIClient + WebApiClient as WebApiClient ) +from .config import WebApiConfig as WebApiConfig diff --git a/src/home/api/config.py b/src/home/api/config.py new file mode 100644 index 0000000..00c1097 --- /dev/null +++ b/src/home/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/web_api_client.py b/src/home/api/web_api_client.py index 6677182..15c1915 100644 --- a/src/home/api/web_api_client.py +++ b/src/home/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,22 +37,22 @@ 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 # ----------- @@ -152,7 +154,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 +198,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 +206,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/database/_base.py b/src/home/database/_base.py new file mode 100644 index 0000000..c01e62b --- /dev/null +++ b/src/home/database/_base.py @@ -0,0 +1,9 @@ +import os + + +def get_data_root_directory(name: str) -> str: + return os.path.join( + os.environ['HOME'], + '.config', + 'homekit', + 'data') \ No newline at end of file diff --git a/src/home/database/simple_state.py b/src/home/database/simple_state.py index cada9c8..2b8ebe7 100644 --- a/src/home/database/simple_state.py +++ b/src/home/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/src/home/database/sqlite.py index 8c6145c..0af1f54 100644 --- a/src/home/database/sqlite.py +++ b/src/home/database/sqlite.py @@ -2,15 +2,13 @@ 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) -> str: return os.path.join( - os.environ['HOME'], - '.config', - 'homekit', - 'data', + get_data_root_directory(), f'{name}.db') diff --git a/src/home/telegram/_botutil.py b/src/home/telegram/_botutil.py index 6d1ee8f..b551a55 100644 --- a/src/home/telegram/_botutil.py +++ b/src/home/telegram/_botutil.py @@ -3,7 +3,7 @@ import traceback from html import escape from telegram import User -from home.api import WebAPIClient as APIClient +from home.api import WebApiClient as APIClient from home.api.types import BotType from home.api.errors import ApiResponseError diff --git a/src/home/telegram/bot.py b/src/home/telegram/bot.py index 7e22263..e6ebc6e 100644 --- a/src/home/telegram/bot.py +++ b/src/home/telegram/bot.py @@ -21,7 +21,7 @@ from telegram.ext.filters import BaseFilter from telegram.error import TimedOut from home.config import config -from home.api import WebAPIClient +from home.api import WebApiClient from home.api.types import BotType from ._botlang import lang, languages @@ -522,7 +522,7 @@ def _logging_callback_handler(update: Update, context: CallbackContext): def enable_logging(bot_type: BotType): - api = WebAPIClient(timeout=3) + api = WebApiClient(timeout=3) api.enable_async() global _reporting diff --git a/src/inverter_bot.py b/src/inverter_bot.py index d35e606..1dd167e 100755 --- a/src/inverter_bot.py +++ b/src/inverter_bot.py @@ -28,7 +28,7 @@ from home.inverter.types import ( ) from home.database.inverter_time_formats import FormatDate from home.api.types import BotType -from home.api import WebAPIClient +from home.api import WebApiClient from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton @@ -718,7 +718,7 @@ class ConsumptionConversation(bot.conversation): message = 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: diff --git a/src/openwrt_log_analyzer.py b/src/openwrt_log_analyzer.py index 35b755f..c1c4fbe 100755 --- a/src/openwrt_log_analyzer.py +++ b/src/openwrt_log_analyzer.py @@ -59,7 +59,7 @@ if __name__ == '__main__': state_file = config['simple_state']['file'] state_file = state_file.replace('.txt', f'-{ap}.txt') - state = SimpleState(file=state_file, + state = SimpleState(name=state_file, default={'last_id': 0}) max_last_id = 0 diff --git a/src/openwrt_logger.py b/src/openwrt_logger.py index 97fe7a9..82f11ac 100755 --- a/src/openwrt_logger.py +++ b/src/openwrt_logger.py @@ -2,29 +2,19 @@ import os 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.config import config, AppConfigUnit from home.database import SimpleState -from home.api import WebAPIClient +from home.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 +36,10 @@ if __name__ == '__main__': parser.add_argument('--access-point', type=int, required=True, help='access point number') - arg = config.load_app('openwrt_logger', parser=parser) - - state = SimpleState(file=config['simple_state']['file'].replace('{ap}', str(arg.access_point)), - default={'seek': 0, 'size': 0}) + arg = config.load_app(OpenwrtLoggerConfig, parser=parser) + state = SimpleState(name=config.app_config['database_name_template'].replace('{ap}', str(arg.access_point)), + default=dict(seek=0, size=0)) fsize = os.path.getsize(arg.file) if fsize < state['size']: state['seek'] = 0 @@ -79,5 +68,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/sensors_bot.py b/src/sensors_bot.py index 152dd24..441c212 100755 --- a/src/sensors_bot.py +++ b/src/sensors_bot.py @@ -17,7 +17,7 @@ from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardBu from home.config import config from home.telegram import bot from home.util import chunks, MySimpleSocketClient -from home.api import WebAPIClient +from home.api import WebApiClient from home.api.types import ( BotType, TemperatureSensorLocation @@ -111,7 +111,7 @@ def callback_handler(ctx: bot.Context) -> None: sensor = TemperatureSensorLocation[match.group(1).upper()] hours = int(match.group(2)) - api = WebAPIClient(timeout=20) + api = WebApiClient(timeout=20) data = api.get_sensors_data(sensor, hours) title = ctx.lang(sensor.name.lower()) + ' (' + ctx.lang('n_hrs', hours) + ')' diff --git a/src/sound_bot.py b/src/sound_bot.py index 32371bd..bc9edce 100755 --- a/src/sound_bot.py +++ b/src/sound_bot.py @@ -9,7 +9,7 @@ from html import escape from typing import Optional, List, Dict, Tuple from home.config import config -from home.api import WebAPIClient +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 @@ -734,7 +734,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 +757,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) diff --git a/src/sound_sensor_server.py b/src/sound_sensor_server.py index b660210..3446b80 100755 --- a/src/sound_sensor_server.py +++ b/src/sound_sensor_server.py @@ -7,7 +7,7 @@ from typing import Optional, List, Dict, Tuple from functools import partial from home.config import config from home.util import Addr -from home.api import WebAPIClient, RequestParams +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 @@ -120,7 +120,7 @@ def hits_sender(): sleep(5) -api: Optional[WebAPIClient] = None +api: Optional[WebApiClient] = None hc: Optional[HitCounter] = None record_clients: Dict[MediaNodeType, RecordClient] = {} @@ -162,7 +162,7 @@ if __name__ == '__main__': config.load_app('sound_sensor_server') hc = HitCounter() - api = WebAPIClient(timeout=(10, 60)) + api = WebApiClient(timeout=(10, 60)) api.enable_async(error_handler=api_error_handler) t = threading.Thread(target=hits_sender) diff --git a/test/test_api.py b/test/test_api.py index e80eb4c..ecf8764 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -7,7 +7,7 @@ sys.path.extend([ ) ]) -from src.home.api import WebAPIClient +from src.home.api import WebApiClient from src.home.api.types import BotType from src.home.config import config @@ -15,5 +15,5 @@ from src.home.config import config if __name__ == '__main__': config.load_app('test_api') - api = WebAPIClient() + api = WebApiClient() print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) diff --git a/test/test_record_upload.py b/test/test_record_upload.py index 835504f..c0daceb 100755 --- a/test/test_record_upload.py +++ b/test/test_record_upload.py @@ -10,7 +10,7 @@ sys.path.extend([ import time -from src.home.api import WebAPIClient, RequestParams +from src.home.api import WebApiClient, RequestParams from src.home.config import config from src.home.media import SoundRecordClient from src.home.util import Addr @@ -74,7 +74,7 @@ if __name__ == '__main__': 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_sound_server_api.py b/test/test_sound_server_api.py index 5295a5d..77fe1ba 100755 --- a/test/test_sound_server_api.py +++ b/test/test_sound_server_api.py @@ -10,7 +10,7 @@ import threading from time import sleep from src.home.config import config -from src.home.api import WebAPIClient +from src.home.api import WebApiClient from src.home.api.types import SoundSensorLocation from typing import List, Tuple @@ -59,7 +59,7 @@ if __name__ == '__main__': config.load_app('test_api') hc = HitCounter() - api = WebAPIClient() + api = WebApiClient() hc.add('spb1', 1) # hc.add('big_house', 123) From f3b9d50496257d87757802dfb472b5ffae11962c Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 22:44:31 +0300 Subject: [PATCH 05/51] new config: port openwrt_log_analyzer --- src/home/telegram/telegram.py | 28 ++++++++------- src/home/util.py | 8 +++++ src/openwrt_log_analyzer.py | 64 +++++++++++++++++++---------------- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/home/telegram/telegram.py b/src/home/telegram/telegram.py index 2f94f93..f42363e 100644 --- a/src/home/telegram/telegram.py +++ b/src/home/telegram/telegram.py @@ -2,25 +2,27 @@ import requests import logging from typing import Tuple -from ..config import config - +from .config import TelegramChatsConfig +_chats = TelegramChatsConfig() _logger = logging.getLogger(__name__) def send_message(text: str, - parse_mode: str = None, - disable_web_page_preview: bool = False): - data, token = _send_telegram_data(text, parse_mode, disable_web_page_preview) + chat: str, + parse_mode: str = 'HTML', + disable_web_page_preview: bool = False,): + data, token = _send_telegram_data(text, chat, parse_mode, disable_web_page_preview) req = requests.post('https://api.telegram.org/bot%s/sendMessage' % token, data=data) return req.json() -def send_photo(filename: str): +def send_photo(filename: str, chat: str): + chat_data = _chats[chat] data = { - 'chat_id': config['telegram']['chat_id'], + 'chat_id': chat_data['id'], } - token = config['telegram']['token'] + token = chat_data['token'] url = f'https://api.telegram.org/bot{token}/sendPhoto' with open(filename, "rb") as fd: @@ -29,19 +31,19 @@ def send_photo(filename: str): def _send_telegram_data(text: str, + chat: str, parse_mode: str = None, disable_web_page_preview: bool = False) -> Tuple[dict, str]: + chat_data = _chats[chat] data = { - 'chat_id': config['telegram']['chat_id'], + 'chat_id': chat_data['id'], 'text': text } if parse_mode is not None: data['parse_mode'] = parse_mode - elif 'parse_mode' in config['telegram']: - data['parse_mode'] = config['telegram']['parse_mode'] - if disable_web_page_preview or 'disable_web_page_preview' in config['telegram']: + if disable_web_page_preview: data['disable_web_page_preview'] = 1 - return data, config['telegram']['token'] + return data, chat_data['token'] diff --git a/src/home/util.py b/src/home/util.py index 1e12243..11e7116 100644 --- a/src/home/util.py +++ b/src/home/util.py @@ -36,6 +36,14 @@ def validate_ipv4_or_hostname(address: str, raise_exception: bool = False) -> bo 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] diff --git a/src/openwrt_log_analyzer.py b/src/openwrt_log_analyzer.py index c1c4fbe..96023cd 100755 --- a/src/openwrt_log_analyzer.py +++ b/src/openwrt_log_analyzer.py @@ -1,33 +1,39 @@ #!/usr/bin/env python3 import home.telegram as telegram -from home.config import config +from home.telegram.config import TelegramChatsConfig +from home.util import validate_mac_address +from typing import Optional +from home.config import config, AppConfigUnit from home.database import BotsDatabase, SimpleState -""" -config.toml example: -[simple_state] -file = "/home/user/.config/openwrt_log_analyzer/state.txt" +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'} + } + } -[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 -""" + @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, @@ -48,18 +54,18 @@ def main(mac: str, max_id = log.id text = '\n'.join(map(lambda s: str(s), data)) - telegram.send_message(f'{title} (AP #{ap})\n\n' + text) + telegram.send_message(f'{title} (AP #{ap})\n\n' + text, config.app_config['telegram_chat']) return max_id if __name__ == '__main__': - config.load_app('openwrt_log_analyzer') - for ap in config['openwrt_log_analyzer']['aps']: - state_file = config['simple_state']['file'] - state_file = state_file.replace('.txt', f'-{ap}.txt') + 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=state_file, + state = SimpleState(name=dbname, default={'last_id': 0}) max_last_id = 0 From b0bf43e6a272d42a55158e657bd937cb82fc3d8d Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 23:02:34 +0300 Subject: [PATCH 06/51] move files, rename home package to homekit --- .gitignore | 5 +++-- bin/__py_include.py | 9 +++++++++ {src => bin}/camera_node.py | 11 +++++----- {src => bin}/electricity_calc.py | 5 +++-- {src => bin}/esp32_capture.py | 5 +++-- {src => bin}/esp32cam_capture_diff_node.py | 9 +++++---- {src => bin}/gpiorelayd.py | 5 +++-- {src => bin}/inverter_bot.py | 19 +++++++++--------- {src => bin}/inverter_mqtt_util.py | 10 ++++++---- {src => bin}/inverterd_emulator.py | 3 ++- {src => bin}/ipcam_server.py | 12 ++++++----- {src => bin}/mqtt_node_util.py | 7 ++++--- {src => bin}/openwrt_log_analyzer.py | 11 +++++----- {src => bin}/openwrt_logger.py | 7 ++++--- {src => bin}/pio_build.py | 1 + {src => bin}/pio_ini.py | 5 +++-- {src => bin}/polaris_kettle_bot.py | 11 +++++----- {src => bin}/polaris_kettle_util.py | 5 +++-- {src => bin}/pump_bot.py | 20 ++++++++++--------- {src => bin}/pump_mqtt_bot.py | 13 ++++++------ {src => bin}/relay_mqtt_bot.py | 15 +++++++------- {src => bin}/relay_mqtt_http_proxy.py | 11 +++++----- {src => bin}/sensors_bot.py | 11 +++++----- {src => bin}/sound_bot.py | 17 ++++++++-------- {src => bin}/sound_node.py | 9 +++++---- {src => bin}/sound_sensor_node.py | 7 ++++--- {src => bin}/sound_sensor_server.py | 13 ++++++------ {src => bin}/ssh_tunnels_config_util.py | 4 ++-- {src => bin}/temphum_mqtt_node.py | 7 ++++--- {src => bin}/temphum_mqtt_receiver.py | 5 +++-- {src => bin}/temphum_nodes_util.py | 4 +++- {src => bin}/temphum_smbus_util.py | 6 ++++-- {src => bin}/temphumd.py | 7 ++++--- {src => bin}/web_api.py | 13 ++++++------ {assets => misc}/mqtt_ca.crt | 0 {src => py_include}/__init__.py | 0 {src/home => py_include/homekit}/__init__.py | 0 .../homekit}/api/__init__.py | 0 .../homekit}/api/__init__.pyi | 0 .../home => py_include/homekit}/api/config.py | 0 .../homekit}/api/errors/__init__.py | 0 .../homekit}/api/errors/api_response_error.py | 0 .../homekit}/api/types/__init__.py | 0 .../homekit}/api/types/types.py | 0 .../homekit}/api/web_api_client.py | 0 .../homekit}/audio/__init__.py | 0 .../homekit}/audio/amixer.py | 0 .../homekit}/camera/__init__.py | 0 .../homekit}/camera/esp32.py | 0 .../homekit}/camera/types.py | 0 .../homekit}/camera/util.py | 0 .../homekit}/config/__init__.py | 0 .../homekit}/config/_configs.py | 0 .../homekit}/config/config.py | 0 .../homekit}/database/__init__.py | 0 .../homekit}/database/__init__.pyi | 0 .../homekit}/database/_base.py | 0 .../homekit}/database/bots.py | 0 .../homekit}/database/clickhouse.py | 0 .../homekit}/database/inverter.py | 0 .../database/inverter_time_formats.py | 0 .../homekit}/database/mysql.py | 0 .../homekit}/database/sensors.py | 0 .../homekit}/database/simple_state.py | 0 .../homekit}/database/sqlite.py | 0 .../homekit}/http/__init__.py | 0 {src/home => py_include/homekit}/http/http.py | 0 .../homekit}/inverter/__init__.py | 0 .../homekit}/inverter/config.py | 0 .../homekit}/inverter/emulator.py | 0 .../homekit}/inverter/inverter_wrapper.py | 0 .../homekit}/inverter/monitor.py | 0 .../homekit}/inverter/types.py | 0 .../homekit}/inverter/util.py | 0 .../homekit}/media/__init__.py | 0 .../homekit}/media/__init__.pyi | 0 .../homekit}/media/node_client.py | 0 .../homekit}/media/node_server.py | 0 .../homekit}/media/record.py | 0 .../homekit}/media/record_client.py | 0 .../homekit}/media/storage.py | 0 .../homekit}/media/types.py | 0 .../homekit}/mqtt/__init__.py | 0 .../homekit}/mqtt/_config.py | 0 .../homekit}/mqtt/_module.py | 0 .../home => py_include/homekit}/mqtt/_mqtt.py | 2 +- .../home => py_include/homekit}/mqtt/_node.py | 0 .../homekit}/mqtt/_payload.py | 0 .../home => py_include/homekit}/mqtt/_util.py | 0 .../homekit}/mqtt/_wrapper.py | 0 .../homekit}/mqtt/module/diagnostics.py | 0 .../homekit}/mqtt/module/inverter.py | 2 +- .../homekit}/mqtt/module/ota.py | 0 .../homekit}/mqtt/module/relay.py | 0 .../homekit}/mqtt/module/temphum.py | 0 .../homekit}/pio/__init__.py | 0 .../homekit}/pio/exceptions.py | 0 .../homekit}/pio/products.py | 0 .../homekit}/relay/__init__.py | 0 .../homekit}/relay/__init__.pyi | 0 .../homekit}/relay/sunxi_h3_client.py | 0 .../homekit}/relay/sunxi_h3_server.py | 0 .../homekit}/soundsensor/__init__.py | 0 .../homekit}/soundsensor/__init__.pyi | 0 .../homekit}/soundsensor/node.py | 0 .../homekit}/soundsensor/server.py | 0 .../homekit}/soundsensor/server_client.py | 0 .../homekit}/telegram/__init__.py | 0 .../homekit}/telegram/_botcontext.py | 0 .../homekit}/telegram/_botdb.py | 2 +- .../homekit}/telegram/_botlang.py | 0 .../homekit}/telegram/_botutil.py | 6 +++--- .../homekit}/telegram/aio.py | 0 .../homekit}/telegram/bot.py | 6 +++--- .../homekit}/telegram/config.py | 0 .../homekit}/telegram/telegram.py | 0 .../homekit}/temphum/__init__.py | 0 .../homekit}/temphum/base.py | 0 .../homekit}/temphum/i2c.py | 0 {src/home => py_include/homekit}/util.py | 0 {pyA20 => py_include/pyA20}/__init__.pyi | 0 .../pyA20}/gpio/connector.pyi | 0 {pyA20 => py_include/pyA20}/gpio/gpio.pyi | 0 {pyA20 => py_include/pyA20}/gpio/port.pyi | 0 {pyA20 => py_include/pyA20}/port.pyi | 0 {src => py_include}/syncleo/__init__.py | 0 {src => py_include}/syncleo/kettle.py | 0 {src => py_include}/syncleo/protocol.py | 0 systemd/camera_node.service | 2 +- systemd/camera_node@.service | 2 +- systemd/esp32cam_capture_diff_node.service | 2 +- systemd/gpiorelayd@.service | 2 +- systemd/inverter_bot.service | 2 +- systemd/inverter_mqtt_receiver.service | 2 +- systemd/inverter_mqtt_sender.service | 2 +- systemd/ipcam_server.service | 2 +- systemd/polaris_kettle_bot.service | 2 +- systemd/pump_bot.service | 2 +- systemd/pump_mqtt_bot.service | 2 +- systemd/relay_mqtt_bot.service | 2 +- systemd/relay_mqtt_http_proxy.service | 2 +- systemd/sensors_bot.service | 2 +- systemd/sound_bot.service | 2 +- systemd/sound_node.service | 2 +- systemd/sound_sensor_node.service | 2 +- systemd/sound_sensor_server.service | 2 +- systemd/temphumd.service | 2 +- systemd/temphumd@.service | 2 +- test/__init__.py | 0 test/test.py | 2 +- test/test_stopwatch.py | 2 +- 151 files changed, 205 insertions(+), 159 deletions(-) create mode 100644 bin/__py_include.py rename {src => bin}/camera_node.py (91%) rename {src => bin}/electricity_calc.py (97%) rename {src => bin}/esp32_capture.py (94%) rename {src => bin}/esp32cam_capture_diff_node.py (93%) rename {src => bin}/gpiorelayd.py (80%) rename {src => bin}/inverter_bot.py (98%) rename {src => bin}/inverter_mqtt_util.py (69%) rename {src => bin}/inverterd_emulator.py (68%) rename {src => bin}/ipcam_server.py (98%) rename {src => bin}/mqtt_node_util.py (92%) rename {src => bin}/openwrt_log_analyzer.py (89%) rename {src => bin}/openwrt_logger.py (92%) rename {src => bin}/pio_build.py (77%) rename {src => bin}/pio_ini.py (97%) rename {src => bin}/polaris_kettle_bot.py (99%) rename {src => bin}/polaris_kettle_util.py (97%) rename {src => bin}/pump_bot.py (93%) rename {src => bin}/pump_mqtt_bot.py (94%) rename {src => bin}/relay_mqtt_bot.py (91%) rename {src => bin}/relay_mqtt_http_proxy.py (91%) rename {src => bin}/sensors_bot.py (95%) rename {src => bin}/sound_bot.py (98%) rename {src => bin}/sound_node.py (93%) rename {src => bin}/sound_sensor_node.py (86%) rename {src => bin}/sound_sensor_server.py (94%) rename {src => bin}/ssh_tunnels_config_util.py (94%) rename {src => bin}/temphum_mqtt_node.py (92%) rename {src => bin}/temphum_mqtt_receiver.py (93%) rename {src => bin}/temphum_nodes_util.py (87%) rename {src => bin}/temphum_smbus_util.py (85%) rename {src => bin}/temphumd.py (92%) rename {src => bin}/web_api.py (95%) rename {assets => misc}/mqtt_ca.crt (100%) rename {src => py_include}/__init__.py (100%) rename {src/home => py_include/homekit}/__init__.py (100%) rename {src/home => py_include/homekit}/api/__init__.py (100%) rename {src/home => py_include/homekit}/api/__init__.pyi (100%) rename {src/home => py_include/homekit}/api/config.py (100%) rename {src/home => py_include/homekit}/api/errors/__init__.py (100%) rename {src/home => py_include/homekit}/api/errors/api_response_error.py (100%) rename {src/home => py_include/homekit}/api/types/__init__.py (100%) rename {src/home => py_include/homekit}/api/types/types.py (100%) rename {src/home => py_include/homekit}/api/web_api_client.py (100%) rename {src/home => py_include/homekit}/audio/__init__.py (100%) rename {src/home => py_include/homekit}/audio/amixer.py (100%) rename {src/home => py_include/homekit}/camera/__init__.py (100%) rename {src/home => py_include/homekit}/camera/esp32.py (100%) rename {src/home => py_include/homekit}/camera/types.py (100%) rename {src/home => py_include/homekit}/camera/util.py (100%) rename {src/home => py_include/homekit}/config/__init__.py (100%) rename {src/home => py_include/homekit}/config/_configs.py (100%) rename {src/home => py_include/homekit}/config/config.py (100%) rename {src/home => py_include/homekit}/database/__init__.py (100%) rename {src/home => py_include/homekit}/database/__init__.pyi (100%) rename {src/home => py_include/homekit}/database/_base.py (100%) rename {src/home => py_include/homekit}/database/bots.py (100%) rename {src/home => py_include/homekit}/database/clickhouse.py (100%) rename {src/home => py_include/homekit}/database/inverter.py (100%) rename {src/home => py_include/homekit}/database/inverter_time_formats.py (100%) rename {src/home => py_include/homekit}/database/mysql.py (100%) rename {src/home => py_include/homekit}/database/sensors.py (100%) rename {src/home => py_include/homekit}/database/simple_state.py (100%) rename {src/home => py_include/homekit}/database/sqlite.py (100%) rename {src/home => py_include/homekit}/http/__init__.py (100%) rename {src/home => py_include/homekit}/http/http.py (100%) rename {src/home => py_include/homekit}/inverter/__init__.py (100%) rename {src/home => py_include/homekit}/inverter/config.py (100%) rename {src/home => py_include/homekit}/inverter/emulator.py (100%) rename {src/home => py_include/homekit}/inverter/inverter_wrapper.py (100%) rename {src/home => py_include/homekit}/inverter/monitor.py (100%) rename {src/home => py_include/homekit}/inverter/types.py (100%) rename {src/home => py_include/homekit}/inverter/util.py (100%) rename {src/home => py_include/homekit}/media/__init__.py (100%) rename {src/home => py_include/homekit}/media/__init__.pyi (100%) rename {src/home => py_include/homekit}/media/node_client.py (100%) rename {src/home => py_include/homekit}/media/node_server.py (100%) rename {src/home => py_include/homekit}/media/record.py (100%) rename {src/home => py_include/homekit}/media/record_client.py (100%) rename {src/home => py_include/homekit}/media/storage.py (100%) rename {src/home => py_include/homekit}/media/types.py (100%) rename {src/home => py_include/homekit}/mqtt/__init__.py (100%) rename {src/home => py_include/homekit}/mqtt/_config.py (100%) rename {src/home => py_include/homekit}/mqtt/_module.py (100%) rename {src/home => py_include/homekit}/mqtt/_mqtt.py (99%) rename {src/home => py_include/homekit}/mqtt/_node.py (100%) rename {src/home => py_include/homekit}/mqtt/_payload.py (100%) rename {src/home => py_include/homekit}/mqtt/_util.py (100%) rename {src/home => py_include/homekit}/mqtt/_wrapper.py (100%) rename {src/home => py_include/homekit}/mqtt/module/diagnostics.py (100%) rename {src/home => py_include/homekit}/mqtt/module/inverter.py (99%) rename {src/home => py_include/homekit}/mqtt/module/ota.py (100%) rename {src/home => py_include/homekit}/mqtt/module/relay.py (100%) rename {src/home => py_include/homekit}/mqtt/module/temphum.py (100%) rename {src/home => py_include/homekit}/pio/__init__.py (100%) rename {src/home => py_include/homekit}/pio/exceptions.py (100%) rename {src/home => py_include/homekit}/pio/products.py (100%) rename {src/home => py_include/homekit}/relay/__init__.py (100%) rename {src/home => py_include/homekit}/relay/__init__.pyi (100%) rename {src/home => py_include/homekit}/relay/sunxi_h3_client.py (100%) rename {src/home => py_include/homekit}/relay/sunxi_h3_server.py (100%) rename {src/home => py_include/homekit}/soundsensor/__init__.py (100%) rename {src/home => py_include/homekit}/soundsensor/__init__.pyi (100%) rename {src/home => py_include/homekit}/soundsensor/node.py (100%) rename {src/home => py_include/homekit}/soundsensor/server.py (100%) rename {src/home => py_include/homekit}/soundsensor/server_client.py (100%) rename {src/home => py_include/homekit}/telegram/__init__.py (100%) rename {src/home => py_include/homekit}/telegram/_botcontext.py (100%) rename {src/home => py_include/homekit}/telegram/_botdb.py (95%) rename {src/home => py_include/homekit}/telegram/_botlang.py (100%) rename {src/home => py_include/homekit}/telegram/_botutil.py (88%) rename {src/home => py_include/homekit}/telegram/aio.py (100%) rename {src/home => py_include/homekit}/telegram/bot.py (99%) rename {src/home => py_include/homekit}/telegram/config.py (100%) rename {src/home => py_include/homekit}/telegram/telegram.py (100%) rename {src/home => py_include/homekit}/temphum/__init__.py (100%) rename {src/home => py_include/homekit}/temphum/base.py (100%) rename {src/home => py_include/homekit}/temphum/i2c.py (100%) rename {src/home => py_include/homekit}/util.py (100%) rename {pyA20 => py_include/pyA20}/__init__.pyi (100%) rename {pyA20 => py_include/pyA20}/gpio/connector.pyi (100%) rename {pyA20 => py_include/pyA20}/gpio/gpio.pyi (100%) rename {pyA20 => py_include/pyA20}/gpio/port.pyi (100%) rename {pyA20 => py_include/pyA20}/port.pyi (100%) rename {src => py_include}/syncleo/__init__.py (100%) rename {src => py_include}/syncleo/kettle.py (100%) rename {src => py_include}/syncleo/protocol.py (100%) delete mode 100644 test/__init__.py diff --git a/.gitignore b/.gitignore index 4ffc1b1..1280ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,11 @@ config.def.h __pycache__ .DS_Store -/src/test/test_inverter_monitor.log +/py_include/test/test_inverter_monitor.log /youtrack-certificate /cpp -/src/test.py +/py_include/test.py +/bin/test.py /esp32-cam/CameraWebServer/wifi_password.h cmake-build-* .pio diff --git a/bin/__py_include.py b/bin/__py_include.py new file mode 100644 index 0000000..7f95e28 --- /dev/null +++ b/bin/__py_include.py @@ -0,0 +1,9 @@ +import sys +import os.path + +for _name in ('py_include',): + 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 similarity index 91% rename from src/camera_node.py rename to bin/camera_node.py index 3f2c5a4..1485557 100755 --- a/src/camera_node.py +++ b/bin/camera_node.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import asyncio import time +import __py_include -from home.config import config -from home.media import MediaNodeServer, ESP32CameraRecordStorage, CameraRecorder -from home.camera import CameraType, esp32 -from home.util import Addr -from home import http +from homekit.config import config +from homekit.media import MediaNodeServer, ESP32CameraRecordStorage, CameraRecorder +from homekit.camera import CameraType, esp32 +from homekit.util import Addr +from homekit import http # Implements HTTP API for a camera. diff --git a/src/electricity_calc.py b/bin/electricity_calc.py similarity index 97% rename from src/electricity_calc.py rename to bin/electricity_calc.py index 8ea5a1c..cff2327 100755 --- a/src/electricity_calc.py +++ b/bin/electricity_calc.py @@ -3,11 +3,12 @@ import logging import os import sys import inspect +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 similarity index 94% rename from src/esp32_capture.py rename to bin/esp32_capture.py index 0441565..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 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 diff --git a/src/esp32cam_capture_diff_node.py b/bin/esp32cam_capture_diff_node.py similarity index 93% rename from src/esp32cam_capture_diff_node.py rename to bin/esp32cam_capture_diff_node.py index 59482f7..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 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 diff --git a/src/gpiorelayd.py b/bin/gpiorelayd.py similarity index 80% rename from src/gpiorelayd.py rename to bin/gpiorelayd.py index f1a9e57..1f4d2e2 100755 --- a/src/gpiorelayd.py +++ b/bin/gpiorelayd.py @@ -2,9 +2,10 @@ import logging import os import sys +import __py_include -from home.config import config -from home.relay.sunxi_h3_server import RelayServer +from homekit.config import config +from homekit.relay.sunxi_h3_server import RelayServer logger = logging.getLogger(__name__) diff --git a/src/inverter_bot.py b/bin/inverter_bot.py similarity index 98% rename from src/inverter_bot.py rename to bin/inverter_bot.py index 1dd167e..fdfe436 100755 --- a/src/inverter_bot.py +++ b/bin/inverter_bot.py @@ -5,30 +5,31 @@ import datetime import json import itertools import sys +import __py_include from inverterd import Format, InverterError from html import escape from typing import Optional, Tuple, Union -from home.util import chunks -from home.config import config, AppConfigUnit -from home.telegram import bot -from home.telegram.config import TelegramBotConfig, TelegramUserListType -from home.inverter import ( +from homekit.util import chunks +from homekit.config import config, AppConfigUnit +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig, TelegramUserListType +from homekit.inverter import ( wrapper_instance as inverter, beautify_table, InverterMonitor, ) -from home.inverter.types import ( +from homekit.inverter.types import ( ChargingEvent, ACPresentEvent, BatteryState, ACMode, OutputSourcePriority ) -from home.database.inverter_time_formats import FormatDate -from home.api.types import BotType -from home.api import WebApiClient +from homekit.database.inverter_time_formats import FormatDate +from homekit.api.types import BotType +from homekit.api import WebApiClient from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton diff --git a/src/inverter_mqtt_util.py b/bin/inverter_mqtt_util.py similarity index 69% rename from src/inverter_mqtt_util.py rename to bin/inverter_mqtt_util.py index 791bf80..6003c62 100755 --- a/src/inverter_mqtt_util.py +++ b/bin/inverter_mqtt_util.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import __py_include + from argparse import ArgumentParser -from home.config import config, app_config -from home.mqtt import MqttWrapper, MqttNode +from homekit.config import config +from homekit.mqtt import MqttWrapper, MqttNode if __name__ == '__main__': @@ -17,8 +19,8 @@ if __name__ == '__main__': node = MqttNode(node_id='inverter') module_kwargs = {} if mode == 'sender': - module_kwargs['status_poll_freq'] = int(app_config['poll_freq']) - module_kwargs['generation_poll_freq'] = int(app_config['generation_poll_freq']) + module_kwargs['status_poll_freq'] = int(config.app_config['poll_freq']) + module_kwargs['generation_poll_freq'] = int(config.app_config['generation_poll_freq']) node.load_module('inverter', **module_kwargs) mqtt.add_node(node) diff --git a/src/inverterd_emulator.py b/bin/inverterd_emulator.py similarity index 68% rename from src/inverterd_emulator.py rename to 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/src/ipcam_server.py b/bin/ipcam_server.py similarity index 98% rename from src/ipcam_server.py rename to bin/ipcam_server.py index a54cd35..211bc86 100755 --- a/src/ipcam_server.py +++ b/bin/ipcam_server.py @@ -5,15 +5,17 @@ import re import asyncio import time import shutil -import home.telegram.aio as telegram +import __py_include + +import homekit.telegram.aio as telegram 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 +from homekit import http +from homekit.database.sqlite import SQLiteBase +from homekit.camera import util as camutil from enum import Enum from typing import Optional, Union, List, Tuple diff --git a/src/mqtt_node_util.py b/bin/mqtt_node_util.py similarity index 92% rename from src/mqtt_node_util.py rename to bin/mqtt_node_util.py index ce954ae..420a87e 100755 --- a/src/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import os.path +import __py_include from time import sleep from typing import Optional from argparse import ArgumentParser, ArgumentError -from home.config import config -from home.mqtt import MqttNode, MqttWrapper, get_mqtt_modules -from home.mqtt import MqttNodesConfig +from homekit.config import config +from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules +from homekit.mqtt import MqttNodesConfig mqtt_node: Optional[MqttNode] = None mqtt: Optional[MqttWrapper] = None diff --git a/src/openwrt_log_analyzer.py b/bin/openwrt_log_analyzer.py similarity index 89% rename from src/openwrt_log_analyzer.py rename to bin/openwrt_log_analyzer.py index 96023cd..5b14a2f 100755 --- a/src/openwrt_log_analyzer.py +++ b/bin/openwrt_log_analyzer.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -import home.telegram as telegram +import __py_include +import homekit.telegram as telegram -from home.telegram.config import TelegramChatsConfig -from home.util import validate_mac_address +from homekit.telegram.config import TelegramChatsConfig +from homekit.util import validate_mac_address from typing import Optional -from home.config import config, AppConfigUnit -from home.database import BotsDatabase, SimpleState +from homekit.config import config, AppConfigUnit +from homekit.database import BotsDatabase, SimpleState class OpenwrtLogAnalyzerConfig(AppConfigUnit): diff --git a/src/openwrt_logger.py b/bin/openwrt_logger.py similarity index 92% rename from src/openwrt_logger.py rename to bin/openwrt_logger.py index 82f11ac..ec67542 100755 --- a/src/openwrt_logger.py +++ b/bin/openwrt_logger.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import os +import __py_include from datetime import datetime from typing import Tuple, List, Optional from argparse import ArgumentParser -from home.config import config, AppConfigUnit -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 class OpenwrtLoggerConfig(AppConfigUnit): diff --git a/src/pio_build.py b/bin/pio_build.py similarity index 77% rename from src/pio_build.py rename to 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 similarity index 97% rename from src/pio_ini.py rename to bin/pio_ini.py index 920c3e5..34ad395 100755 --- a/src/pio_ini.py +++ b/bin/pio_ini.py @@ -2,11 +2,12 @@ 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 def get_config(product: str) -> dict: diff --git a/src/polaris_kettle_bot.py b/bin/polaris_kettle_bot.py similarity index 99% rename from src/polaris_kettle_bot.py rename to bin/polaris_kettle_bot.py index 80baef3..3a24fe0 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,11 @@ import time import threading import paho.mqtt.client as mqtt -from home.telegram import bot -from home.api.types import BotType -from home.mqtt import Mqtt -from home.config import config -from home.util import chunks +from homekit.telegram import bot +from homekit.api.types import BotType +from homekit.mqtt import Mqtt +from homekit.config import config +from homekit.util import chunks from syncleo import ( Kettle, PowerType, diff --git a/src/polaris_kettle_util.py b/bin/polaris_kettle_util.py similarity index 97% rename from src/polaris_kettle_util.py rename to bin/polaris_kettle_util.py index 12c4388..4db0ed4 100755 --- a/src/polaris_kettle_util.py +++ b/bin/polaris_kettle_util.py @@ -4,12 +4,13 @@ import logging import sys import paho.mqtt.client as mqtt +import __py_include from typing import Optional from argparse import ArgumentParser from queue import SimpleQueue -from home.mqtt import Mqtt -from home.config import config +from homekit.mqtt import Mqtt +from homekit.config import config from syncleo import ( Kettle, PowerType, diff --git a/src/pump_bot.py b/bin/pump_bot.py similarity index 93% rename from src/pump_bot.py rename to bin/pump_bot.py index 25f06fd..08d0dc6 100755 --- a/src/pump_bot.py +++ b/bin/pump_bot.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 +import __py_include + from enum import Enum from typing import Optional from telegram import ReplyKeyboardMarkup, User from time import time from datetime import datetime -from home.config import config, is_development_mode -from home.telegram import bot -from home.telegram._botutil import user_any_name -from home.relay.sunxi_h3_client import RelayClient -from home.api.types import BotType -from home.mqtt import MqttNode, MqttWrapper, MqttPayload -from home.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule -from home.mqtt.module.temphum import MqttTemphumDataPayload -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config, is_development_mode +from homekit.telegram import bot +from homekit.telegram._botutil import user_any_name +from homekit.relay.sunxi_h3_client import RelayClient +from homekit.api.types import BotType +from homekit.mqtt import MqttNode, MqttWrapper, MqttPayload +from homekit.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule +from homekit.mqtt.module.temphum import MqttTemphumDataPayload +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload config.load_app('pump_bot') diff --git a/src/pump_mqtt_bot.py b/bin/pump_mqtt_bot.py similarity index 94% rename from src/pump_mqtt_bot.py rename to bin/pump_mqtt_bot.py index 4036d3a..aea1451 100755 --- a/src/pump_mqtt_bot.py +++ b/bin/pump_mqtt_bot.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 import datetime +import __py_include from enum import Enum from typing import Optional from telegram import ReplyKeyboardMarkup, User -from home.config import config -from home.telegram import bot -from home.telegram._botutil import user_any_name -from home.mqtt import MqttNode, MqttPayload -from home.mqtt.module.relay import MqttRelayState -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config +from homekit.telegram import bot +from homekit.telegram._botutil import user_any_name +from homekit.mqtt import MqttNode, MqttPayload +from homekit.mqtt.module.relay import MqttRelayState +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload config.load_app('pump_mqtt_bot') diff --git a/src/relay_mqtt_bot.py b/bin/relay_mqtt_bot.py similarity index 91% rename from src/relay_mqtt_bot.py rename to bin/relay_mqtt_bot.py index 020dc08..1c1cc94 100755 --- a/src/relay_mqtt_bot.py +++ b/bin/relay_mqtt_bot.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 import sys +import __py_include from enum import Enum from typing import Optional, Union from telegram import ReplyKeyboardMarkup from functools import partial -from home.config import config, AppConfigUnit, Translation -from home.telegram import bot -from home.telegram.config import TelegramBotConfig -from home.mqtt import MqttPayload, MqttNode, MqttWrapper, MqttModule -from home.mqtt import MqttNodesConfig -from home.mqtt.module.relay import MqttRelayModule, MqttRelayState -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from homekit.config import config, AppConfigUnit, Translation +from homekit.telegram import bot +from homekit.telegram.config import TelegramBotConfig +from homekit.mqtt import MqttPayload, MqttNode, MqttWrapper, MqttModule +from homekit.mqtt import MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayModule, MqttRelayState +from homekit.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload if __name__ != '__main__': diff --git a/src/relay_mqtt_http_proxy.py b/bin/relay_mqtt_http_proxy.py similarity index 91% rename from src/relay_mqtt_http_proxy.py rename to bin/relay_mqtt_http_proxy.py index e13c04a..23938e1 100755 --- a/src/relay_mqtt_http_proxy.py +++ b/bin/relay_mqtt_http_proxy.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 import logging +import __py_include -from home import http -from home.config import config, AppConfigUnit -from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig -from home.mqtt.module.relay import MqttRelayState, MqttRelayModule, MqttPowerStatusPayload -from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +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 diff --git a/src/sensors_bot.py b/bin/sensors_bot.py similarity index 95% rename from src/sensors_bot.py rename to bin/sensors_bot.py index 441c212..c2b0070 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,11 +15,11 @@ import matplotlib.ticker as mticker from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton -from home.config import config -from home.telegram import bot -from home.util import chunks, MySimpleSocketClient -from home.api import WebApiClient -from home.api.types import ( +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 ( BotType, TemperatureSensorLocation ) diff --git a/src/sound_bot.py b/bin/sound_bot.py similarity index 98% rename from src/sound_bot.py rename to bin/sound_bot.py index bc9edce..518151d 100755 --- a/src/sound_bot.py +++ b/bin/sound_bot.py @@ -2,21 +2,22 @@ import logging import os import tempfile +import __py_include from enum import Enum from datetime import datetime, timedelta from html import escape from typing import Optional, List, Dict, Tuple -from home.config import config -from home.api import WebApiClient -from home.api.types import SoundSensorLocation, BotType -from home.api.errors import ApiResponseError -from home.media import SoundNodeClient, SoundRecordClient, SoundRecordFile, CameraNodeClient -from home.soundsensor import SoundSensorServerGuardClient -from home.util import Addr, chunks, filesize_fmt +from homekit.config import config +from homekit.api import WebApiClient +from homekit.api.types import SoundSensorLocation, BotType +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 diff --git a/src/sound_node.py b/bin/sound_node.py similarity index 93% rename from src/sound_node.py rename to bin/sound_node.py index b0b4a67..90e6997 100755 --- a/src/sound_node.py +++ b/bin/sound_node.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import os +import __py_include from typing import Optional -from home.config import config -from home.audio import amixer -from home.media import MediaNodeServer, SoundRecordStorage, SoundRecorder -from home import http +from homekit.config import config +from homekit.audio import amixer +from homekit.media import MediaNodeServer, SoundRecordStorage, SoundRecorder +from homekit import http # This script must be run as root as it runs arecord. diff --git a/src/sound_sensor_node.py b/bin/sound_sensor_node.py similarity index 86% rename from src/sound_sensor_node.py rename to bin/sound_sensor_node.py index 404fdf4..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 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__) diff --git a/src/sound_sensor_server.py b/bin/sound_sensor_server.py similarity index 94% rename from src/sound_sensor_server.py rename to bin/sound_sensor_server.py index 3446b80..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 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__) diff --git a/src/ssh_tunnels_config_util.py b/bin/ssh_tunnels_config_util.py similarity index 94% rename from src/ssh_tunnels_config_util.py rename to bin/ssh_tunnels_config_util.py index 963c01b..d08a4f4 100755 --- a/src/ssh_tunnels_config_util.py +++ b/bin/ssh_tunnels_config_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 - -from home.config import config +import __py_include +from homekit.config import config if __name__ == '__main__': config.load_app('ssh_tunnels_config_util') diff --git a/src/temphum_mqtt_node.py b/bin/temphum_mqtt_node.py similarity index 92% rename from src/temphum_mqtt_node.py rename to bin/temphum_mqtt_node.py index c3d1975..9ea436d 100755 --- a/src/temphum_mqtt_node.py +++ b/bin/temphum_mqtt_node.py @@ -2,12 +2,13 @@ import asyncio import json import logging +import __py_include from typing import Optional -from home.config import config -from home.temphum import SensorType, BaseSensor -from home.temphum.i2c import create_sensor +from homekit.config import config +from homekit.temphum import SensorType, BaseSensor +from homekit.temphum.i2c import create_sensor logger = logging.getLogger(__name__) sensor: Optional[BaseSensor] = None diff --git a/src/temphum_mqtt_receiver.py b/bin/temphum_mqtt_receiver.py similarity index 93% rename from src/temphum_mqtt_receiver.py rename to bin/temphum_mqtt_receiver.py index 2b30800..d0a378e 100755 --- a/src/temphum_mqtt_receiver.py +++ b/bin/temphum_mqtt_receiver.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 import paho.mqtt.client as mqtt import re +import __py_include -from home.config import config -from home.mqtt import MqttWrapper, MqttNode +from homekit.config import config +from homekit.mqtt import MqttWrapper, MqttNode class MqttServer(Mqtt): diff --git a/src/temphum_nodes_util.py b/bin/temphum_nodes_util.py similarity index 87% rename from src/temphum_nodes_util.py rename to 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 similarity index 85% rename from src/temphum_smbus_util.py rename to bin/temphum_smbus_util.py index c06bacd..1cfaa84 100755 --- a/src/temphum_smbus_util.py +++ b/bin/temphum_smbus_util.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import __py_include + from argparse import ArgumentParser -from home.temphum import SensorType -from home.temphum.i2c import create_sensor +from homekit.temphum import SensorType +from homekit.temphum.i2c import create_sensor if __name__ == '__main__': diff --git a/src/temphumd.py b/bin/temphumd.py similarity index 92% rename from src/temphumd.py rename to bin/temphumd.py index c3d1975..9ea436d 100755 --- a/src/temphumd.py +++ b/bin/temphumd.py @@ -2,12 +2,13 @@ import asyncio import json import logging +import __py_include from typing import Optional -from home.config import config -from home.temphum import SensorType, BaseSensor -from home.temphum.i2c import create_sensor +from homekit.config import config +from homekit.temphum import SensorType, BaseSensor +from homekit.temphum.i2c import create_sensor logger = logging.getLogger(__name__) sensor: Optional[BaseSensor] = None diff --git a/src/web_api.py b/bin/web_api.py similarity index 95% rename from src/web_api.py rename to bin/web_api.py index 0aa994a..0e0fd0b 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 BotType, TemperatureSensorLocation, SoundSensorLocation +from homekit.media import SoundRecordStorage def strptime_auto(s: str) -> datetime: diff --git a/assets/mqtt_ca.crt b/misc/mqtt_ca.crt similarity index 100% rename from assets/mqtt_ca.crt rename to misc/mqtt_ca.crt diff --git a/src/__init__.py b/py_include/__init__.py similarity index 100% rename from src/__init__.py rename to py_include/__init__.py diff --git a/src/home/__init__.py b/py_include/homekit/__init__.py similarity index 100% rename from src/home/__init__.py rename to py_include/homekit/__init__.py diff --git a/src/home/api/__init__.py b/py_include/homekit/api/__init__.py similarity index 100% rename from src/home/api/__init__.py rename to py_include/homekit/api/__init__.py diff --git a/src/home/api/__init__.pyi b/py_include/homekit/api/__init__.pyi similarity index 100% rename from src/home/api/__init__.pyi rename to py_include/homekit/api/__init__.pyi diff --git a/src/home/api/config.py b/py_include/homekit/api/config.py similarity index 100% rename from src/home/api/config.py rename to py_include/homekit/api/config.py diff --git a/src/home/api/errors/__init__.py b/py_include/homekit/api/errors/__init__.py similarity index 100% rename from src/home/api/errors/__init__.py rename to py_include/homekit/api/errors/__init__.py diff --git a/src/home/api/errors/api_response_error.py b/py_include/homekit/api/errors/api_response_error.py similarity index 100% rename from src/home/api/errors/api_response_error.py rename to py_include/homekit/api/errors/api_response_error.py diff --git a/src/home/api/types/__init__.py b/py_include/homekit/api/types/__init__.py similarity index 100% rename from src/home/api/types/__init__.py rename to py_include/homekit/api/types/__init__.py diff --git a/src/home/api/types/types.py b/py_include/homekit/api/types/types.py similarity index 100% rename from src/home/api/types/types.py rename to py_include/homekit/api/types/types.py diff --git a/src/home/api/web_api_client.py b/py_include/homekit/api/web_api_client.py similarity index 100% rename from src/home/api/web_api_client.py rename to py_include/homekit/api/web_api_client.py diff --git a/src/home/audio/__init__.py b/py_include/homekit/audio/__init__.py similarity index 100% rename from src/home/audio/__init__.py rename to py_include/homekit/audio/__init__.py diff --git a/src/home/audio/amixer.py b/py_include/homekit/audio/amixer.py similarity index 100% rename from src/home/audio/amixer.py rename to py_include/homekit/audio/amixer.py diff --git a/src/home/camera/__init__.py b/py_include/homekit/camera/__init__.py similarity index 100% rename from src/home/camera/__init__.py rename to py_include/homekit/camera/__init__.py diff --git a/src/home/camera/esp32.py b/py_include/homekit/camera/esp32.py similarity index 100% rename from src/home/camera/esp32.py rename to py_include/homekit/camera/esp32.py diff --git a/src/home/camera/types.py b/py_include/homekit/camera/types.py similarity index 100% rename from src/home/camera/types.py rename to py_include/homekit/camera/types.py diff --git a/src/home/camera/util.py b/py_include/homekit/camera/util.py similarity index 100% rename from src/home/camera/util.py rename to py_include/homekit/camera/util.py diff --git a/src/home/config/__init__.py b/py_include/homekit/config/__init__.py similarity index 100% rename from src/home/config/__init__.py rename to py_include/homekit/config/__init__.py diff --git a/src/home/config/_configs.py b/py_include/homekit/config/_configs.py similarity index 100% rename from src/home/config/_configs.py rename to py_include/homekit/config/_configs.py diff --git a/src/home/config/config.py b/py_include/homekit/config/config.py similarity index 100% rename from src/home/config/config.py rename to py_include/homekit/config/config.py diff --git a/src/home/database/__init__.py b/py_include/homekit/database/__init__.py similarity index 100% rename from src/home/database/__init__.py rename to py_include/homekit/database/__init__.py diff --git a/src/home/database/__init__.pyi b/py_include/homekit/database/__init__.pyi similarity index 100% rename from src/home/database/__init__.pyi rename to py_include/homekit/database/__init__.pyi diff --git a/src/home/database/_base.py b/py_include/homekit/database/_base.py similarity index 100% rename from src/home/database/_base.py rename to py_include/homekit/database/_base.py diff --git a/src/home/database/bots.py b/py_include/homekit/database/bots.py similarity index 100% rename from src/home/database/bots.py rename to py_include/homekit/database/bots.py diff --git a/src/home/database/clickhouse.py b/py_include/homekit/database/clickhouse.py similarity index 100% rename from src/home/database/clickhouse.py rename to py_include/homekit/database/clickhouse.py diff --git a/src/home/database/inverter.py b/py_include/homekit/database/inverter.py similarity index 100% rename from src/home/database/inverter.py rename to py_include/homekit/database/inverter.py diff --git a/src/home/database/inverter_time_formats.py b/py_include/homekit/database/inverter_time_formats.py similarity index 100% rename from src/home/database/inverter_time_formats.py rename to py_include/homekit/database/inverter_time_formats.py diff --git a/src/home/database/mysql.py b/py_include/homekit/database/mysql.py similarity index 100% rename from src/home/database/mysql.py rename to py_include/homekit/database/mysql.py diff --git a/src/home/database/sensors.py b/py_include/homekit/database/sensors.py similarity index 100% rename from src/home/database/sensors.py rename to py_include/homekit/database/sensors.py diff --git a/src/home/database/simple_state.py b/py_include/homekit/database/simple_state.py similarity index 100% rename from src/home/database/simple_state.py rename to py_include/homekit/database/simple_state.py diff --git a/src/home/database/sqlite.py b/py_include/homekit/database/sqlite.py similarity index 100% rename from src/home/database/sqlite.py rename to py_include/homekit/database/sqlite.py diff --git a/src/home/http/__init__.py b/py_include/homekit/http/__init__.py similarity index 100% rename from src/home/http/__init__.py rename to py_include/homekit/http/__init__.py diff --git a/src/home/http/http.py b/py_include/homekit/http/http.py similarity index 100% rename from src/home/http/http.py rename to py_include/homekit/http/http.py diff --git a/src/home/inverter/__init__.py b/py_include/homekit/inverter/__init__.py similarity index 100% rename from src/home/inverter/__init__.py rename to py_include/homekit/inverter/__init__.py diff --git a/src/home/inverter/config.py b/py_include/homekit/inverter/config.py similarity index 100% rename from src/home/inverter/config.py rename to py_include/homekit/inverter/config.py diff --git a/src/home/inverter/emulator.py b/py_include/homekit/inverter/emulator.py similarity index 100% rename from src/home/inverter/emulator.py rename to py_include/homekit/inverter/emulator.py diff --git a/src/home/inverter/inverter_wrapper.py b/py_include/homekit/inverter/inverter_wrapper.py similarity index 100% rename from src/home/inverter/inverter_wrapper.py rename to py_include/homekit/inverter/inverter_wrapper.py diff --git a/src/home/inverter/monitor.py b/py_include/homekit/inverter/monitor.py similarity index 100% rename from src/home/inverter/monitor.py rename to py_include/homekit/inverter/monitor.py diff --git a/src/home/inverter/types.py b/py_include/homekit/inverter/types.py similarity index 100% rename from src/home/inverter/types.py rename to py_include/homekit/inverter/types.py diff --git a/src/home/inverter/util.py b/py_include/homekit/inverter/util.py similarity index 100% rename from src/home/inverter/util.py rename to py_include/homekit/inverter/util.py diff --git a/src/home/media/__init__.py b/py_include/homekit/media/__init__.py similarity index 100% rename from src/home/media/__init__.py rename to py_include/homekit/media/__init__.py diff --git a/src/home/media/__init__.pyi b/py_include/homekit/media/__init__.pyi similarity index 100% rename from src/home/media/__init__.pyi rename to py_include/homekit/media/__init__.pyi diff --git a/src/home/media/node_client.py b/py_include/homekit/media/node_client.py similarity index 100% rename from src/home/media/node_client.py rename to py_include/homekit/media/node_client.py diff --git a/src/home/media/node_server.py b/py_include/homekit/media/node_server.py similarity index 100% rename from src/home/media/node_server.py rename to py_include/homekit/media/node_server.py diff --git a/src/home/media/record.py b/py_include/homekit/media/record.py similarity index 100% rename from src/home/media/record.py rename to py_include/homekit/media/record.py diff --git a/src/home/media/record_client.py b/py_include/homekit/media/record_client.py similarity index 100% rename from src/home/media/record_client.py rename to py_include/homekit/media/record_client.py diff --git a/src/home/media/storage.py b/py_include/homekit/media/storage.py similarity index 100% rename from src/home/media/storage.py rename to py_include/homekit/media/storage.py diff --git a/src/home/media/types.py b/py_include/homekit/media/types.py similarity index 100% rename from src/home/media/types.py rename to py_include/homekit/media/types.py diff --git a/src/home/mqtt/__init__.py b/py_include/homekit/mqtt/__init__.py similarity index 100% rename from src/home/mqtt/__init__.py rename to py_include/homekit/mqtt/__init__.py diff --git a/src/home/mqtt/_config.py b/py_include/homekit/mqtt/_config.py similarity index 100% rename from src/home/mqtt/_config.py rename to py_include/homekit/mqtt/_config.py diff --git a/src/home/mqtt/_module.py b/py_include/homekit/mqtt/_module.py similarity index 100% rename from src/home/mqtt/_module.py rename to py_include/homekit/mqtt/_module.py diff --git a/src/home/mqtt/_mqtt.py b/py_include/homekit/mqtt/_mqtt.py similarity index 99% rename from src/home/mqtt/_mqtt.py rename to py_include/homekit/mqtt/_mqtt.py index 746ae2e..fb35a24 100644 --- a/src/home/mqtt/_mqtt.py +++ b/py_include/homekit/mqtt/_mqtt.py @@ -45,7 +45,7 @@ class Mqtt: '..', '..', '..', - 'assets', + 'misc', 'mqtt_ca.crt' )) self._client.tls_set(ca_certs=ca_certs, diff --git a/src/home/mqtt/_node.py b/py_include/homekit/mqtt/_node.py similarity index 100% rename from src/home/mqtt/_node.py rename to py_include/homekit/mqtt/_node.py diff --git a/src/home/mqtt/_payload.py b/py_include/homekit/mqtt/_payload.py similarity index 100% rename from src/home/mqtt/_payload.py rename to py_include/homekit/mqtt/_payload.py diff --git a/src/home/mqtt/_util.py b/py_include/homekit/mqtt/_util.py similarity index 100% rename from src/home/mqtt/_util.py rename to py_include/homekit/mqtt/_util.py diff --git a/src/home/mqtt/_wrapper.py b/py_include/homekit/mqtt/_wrapper.py similarity index 100% rename from src/home/mqtt/_wrapper.py rename to py_include/homekit/mqtt/_wrapper.py diff --git a/src/home/mqtt/module/diagnostics.py b/py_include/homekit/mqtt/module/diagnostics.py similarity index 100% rename from src/home/mqtt/module/diagnostics.py rename to py_include/homekit/mqtt/module/diagnostics.py diff --git a/src/home/mqtt/module/inverter.py b/py_include/homekit/mqtt/module/inverter.py similarity index 99% rename from src/home/mqtt/module/inverter.py rename to py_include/homekit/mqtt/module/inverter.py index d927a06..29bde0a 100644 --- a/src/home/mqtt/module/inverter.py +++ b/py_include/homekit/mqtt/module/inverter.py @@ -11,7 +11,7 @@ from .._module import MqttModule from .._node import MqttNode from .._payload import MqttPayload, bit_field try: - from home.database import InverterDatabase + from homekit.database import InverterDatabase except: pass diff --git a/src/home/mqtt/module/ota.py b/py_include/homekit/mqtt/module/ota.py similarity index 100% rename from src/home/mqtt/module/ota.py rename to py_include/homekit/mqtt/module/ota.py diff --git a/src/home/mqtt/module/relay.py b/py_include/homekit/mqtt/module/relay.py similarity index 100% rename from src/home/mqtt/module/relay.py rename to py_include/homekit/mqtt/module/relay.py diff --git a/src/home/mqtt/module/temphum.py b/py_include/homekit/mqtt/module/temphum.py similarity index 100% rename from src/home/mqtt/module/temphum.py rename to py_include/homekit/mqtt/module/temphum.py diff --git a/src/home/pio/__init__.py b/py_include/homekit/pio/__init__.py similarity index 100% rename from src/home/pio/__init__.py rename to py_include/homekit/pio/__init__.py diff --git a/src/home/pio/exceptions.py b/py_include/homekit/pio/exceptions.py similarity index 100% rename from src/home/pio/exceptions.py rename to py_include/homekit/pio/exceptions.py diff --git a/src/home/pio/products.py b/py_include/homekit/pio/products.py similarity index 100% rename from src/home/pio/products.py rename to py_include/homekit/pio/products.py diff --git a/src/home/relay/__init__.py b/py_include/homekit/relay/__init__.py similarity index 100% rename from src/home/relay/__init__.py rename to py_include/homekit/relay/__init__.py diff --git a/src/home/relay/__init__.pyi b/py_include/homekit/relay/__init__.pyi similarity index 100% rename from src/home/relay/__init__.pyi rename to py_include/homekit/relay/__init__.pyi diff --git a/src/home/relay/sunxi_h3_client.py b/py_include/homekit/relay/sunxi_h3_client.py similarity index 100% rename from src/home/relay/sunxi_h3_client.py rename to py_include/homekit/relay/sunxi_h3_client.py diff --git a/src/home/relay/sunxi_h3_server.py b/py_include/homekit/relay/sunxi_h3_server.py similarity index 100% rename from src/home/relay/sunxi_h3_server.py rename to py_include/homekit/relay/sunxi_h3_server.py diff --git a/src/home/soundsensor/__init__.py b/py_include/homekit/soundsensor/__init__.py similarity index 100% rename from src/home/soundsensor/__init__.py rename to py_include/homekit/soundsensor/__init__.py diff --git a/src/home/soundsensor/__init__.pyi b/py_include/homekit/soundsensor/__init__.pyi similarity index 100% rename from src/home/soundsensor/__init__.pyi rename to py_include/homekit/soundsensor/__init__.pyi diff --git a/src/home/soundsensor/node.py b/py_include/homekit/soundsensor/node.py similarity index 100% rename from src/home/soundsensor/node.py rename to py_include/homekit/soundsensor/node.py diff --git a/src/home/soundsensor/server.py b/py_include/homekit/soundsensor/server.py similarity index 100% rename from src/home/soundsensor/server.py rename to py_include/homekit/soundsensor/server.py diff --git a/src/home/soundsensor/server_client.py b/py_include/homekit/soundsensor/server_client.py similarity index 100% rename from src/home/soundsensor/server_client.py rename to py_include/homekit/soundsensor/server_client.py diff --git a/src/home/telegram/__init__.py b/py_include/homekit/telegram/__init__.py similarity index 100% rename from src/home/telegram/__init__.py rename to py_include/homekit/telegram/__init__.py diff --git a/src/home/telegram/_botcontext.py b/py_include/homekit/telegram/_botcontext.py similarity index 100% rename from src/home/telegram/_botcontext.py rename to py_include/homekit/telegram/_botcontext.py diff --git a/src/home/telegram/_botdb.py b/py_include/homekit/telegram/_botdb.py similarity index 95% rename from src/home/telegram/_botdb.py rename to py_include/homekit/telegram/_botdb.py index 9e9cf94..4e1aec0 100644 --- a/src/home/telegram/_botdb.py +++ b/py_include/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/py_include/homekit/telegram/_botlang.py similarity index 100% rename from src/home/telegram/_botlang.py rename to py_include/homekit/telegram/_botlang.py diff --git a/src/home/telegram/_botutil.py b/py_include/homekit/telegram/_botutil.py similarity index 88% rename from src/home/telegram/_botutil.py rename to py_include/homekit/telegram/_botutil.py index b551a55..111a704 100644 --- a/src/home/telegram/_botutil.py +++ b/py_include/homekit/telegram/_botutil.py @@ -3,9 +3,9 @@ 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 +from homekit.api import WebApiClient as APIClient +from homekit.api.types import BotType +from homekit.api.errors import ApiResponseError _logger = logging.getLogger(__name__) diff --git a/src/home/telegram/aio.py b/py_include/homekit/telegram/aio.py similarity index 100% rename from src/home/telegram/aio.py rename to py_include/homekit/telegram/aio.py diff --git a/src/home/telegram/bot.py b/py_include/homekit/telegram/bot.py similarity index 99% rename from src/home/telegram/bot.py rename to py_include/homekit/telegram/bot.py index e6ebc6e..2e33bea 100644 --- a/src/home/telegram/bot.py +++ b/py_include/homekit/telegram/bot.py @@ -20,9 +20,9 @@ from telegram.ext import ( 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 homekit.api import WebApiClient +from homekit.api.types import BotType from ._botlang import lang, languages from ._botdb import BotDatabase diff --git a/src/home/telegram/config.py b/py_include/homekit/telegram/config.py similarity index 100% rename from src/home/telegram/config.py rename to py_include/homekit/telegram/config.py diff --git a/src/home/telegram/telegram.py b/py_include/homekit/telegram/telegram.py similarity index 100% rename from src/home/telegram/telegram.py rename to py_include/homekit/telegram/telegram.py diff --git a/src/home/temphum/__init__.py b/py_include/homekit/temphum/__init__.py similarity index 100% rename from src/home/temphum/__init__.py rename to py_include/homekit/temphum/__init__.py diff --git a/src/home/temphum/base.py b/py_include/homekit/temphum/base.py similarity index 100% rename from src/home/temphum/base.py rename to py_include/homekit/temphum/base.py diff --git a/src/home/temphum/i2c.py b/py_include/homekit/temphum/i2c.py similarity index 100% rename from src/home/temphum/i2c.py rename to py_include/homekit/temphum/i2c.py diff --git a/src/home/util.py b/py_include/homekit/util.py similarity index 100% rename from src/home/util.py rename to py_include/homekit/util.py diff --git a/pyA20/__init__.pyi b/py_include/pyA20/__init__.pyi similarity index 100% rename from pyA20/__init__.pyi rename to py_include/pyA20/__init__.pyi diff --git a/pyA20/gpio/connector.pyi b/py_include/pyA20/gpio/connector.pyi similarity index 100% rename from pyA20/gpio/connector.pyi rename to py_include/pyA20/gpio/connector.pyi diff --git a/pyA20/gpio/gpio.pyi b/py_include/pyA20/gpio/gpio.pyi similarity index 100% rename from pyA20/gpio/gpio.pyi rename to py_include/pyA20/gpio/gpio.pyi diff --git a/pyA20/gpio/port.pyi b/py_include/pyA20/gpio/port.pyi similarity index 100% rename from pyA20/gpio/port.pyi rename to py_include/pyA20/gpio/port.pyi diff --git a/pyA20/port.pyi b/py_include/pyA20/port.pyi similarity index 100% rename from pyA20/port.pyi rename to py_include/pyA20/port.pyi diff --git a/src/syncleo/__init__.py b/py_include/syncleo/__init__.py similarity index 100% rename from src/syncleo/__init__.py rename to py_include/syncleo/__init__.py diff --git a/src/syncleo/kettle.py b/py_include/syncleo/kettle.py similarity index 100% rename from src/syncleo/kettle.py rename to py_include/syncleo/kettle.py diff --git a/src/syncleo/protocol.py b/py_include/syncleo/protocol.py similarity index 100% rename from src/syncleo/protocol.py rename to py_include/syncleo/protocol.py 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..a3a8356 100644 --- a/systemd/gpiorelayd@.service +++ b/systemd/gpiorelayd@.service @@ -6,7 +6,7 @@ After=network-online.target User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/gpiorelayd.py -c /etc/gpiorelayd.conf.d/%i.toml +ExecStart=/home/user/homekit/bin/gpiorelayd.py -c /etc/gpiorelayd.conf.d/%i.toml WorkingDirectory=/root [Install] diff --git a/systemd/inverter_bot.service b/systemd/inverter_bot.service index 96612ae..c5d4aec 100644 --- a/systemd/inverter_bot.service +++ b/systemd/inverter_bot.service @@ -6,7 +6,7 @@ After=inverterd.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/inverter_bot.py +ExecStart=/home/user/homekit/bin/inverter_bot.py WorkingDirectory=/home/user [Install] diff --git a/systemd/inverter_mqtt_receiver.service b/systemd/inverter_mqtt_receiver.service index fedf11f..88f9169 100644 --- a/systemd/inverter_mqtt_receiver.service +++ b/systemd/inverter_mqtt_receiver.service @@ -6,7 +6,7 @@ After=clickhouse-server.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/inverter_mqtt_util.py receiver +ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py receiver WorkingDirectory=/home/user [Install] diff --git a/systemd/inverter_mqtt_sender.service b/systemd/inverter_mqtt_sender.service index 34272bb..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_util.py sender +ExecStart=/home/user/homekit/bin/inverter_mqtt_util.py sender WorkingDirectory=/home/user [Install] diff --git a/systemd/ipcam_server.service b/systemd/ipcam_server.service index 07ac95f..e6f8918 100644 --- a/systemd/ipcam_server.service +++ b/systemd/ipcam_server.service @@ -7,7 +7,7 @@ User=user Group=user Restart=always RestartSec=10 -ExecStart=/home/user/homekit/src/ipcam_server.py +ExecStart=/home/user/homekit/bin/ipcam_server.py WorkingDirectory=/home/user [Install] diff --git a/systemd/polaris_kettle_bot.service b/systemd/polaris_kettle_bot.service index f91ed60..86bb293 100644 --- a/systemd/polaris_kettle_bot.service +++ b/systemd/polaris_kettle_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/polaris_kettle_bot.py +ExecStart=/home/user/homekit/bin/polaris_kettle_bot.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/pump_bot.service b/systemd/pump_bot.service index dd8a46b..b59f5b9 100644 --- a/systemd/pump_bot.service +++ b/systemd/pump_bot.service @@ -6,7 +6,7 @@ After=gpiorelayd.service User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/pump_bot.py +ExecStart=/home/user/homekit/bin/pump_bot.py WorkingDirectory=/home/user [Install] diff --git a/systemd/pump_mqtt_bot.service b/systemd/pump_mqtt_bot.service index 95f9419..6c72cbf 100644 --- a/systemd/pump_mqtt_bot.service +++ b/systemd/pump_mqtt_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/pump_mqtt_bot.py +ExecStart=/home/user/homekit/bin/pump_mqtt_bot.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/relay_mqtt_bot.service b/systemd/relay_mqtt_bot.service index 93696ac..3bac158 100644 --- a/systemd/relay_mqtt_bot.service +++ b/systemd/relay_mqtt_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/relay_mqtt_bot.py +ExecStart=/home/user/homekit/bin/relay_mqtt_bot.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/relay_mqtt_http_proxy.service b/systemd/relay_mqtt_http_proxy.service index 316a920..8301d52 100644 --- a/systemd/relay_mqtt_http_proxy.service +++ b/systemd/relay_mqtt_http_proxy.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/relay_mqtt_http_proxy.py +ExecStart=/home/user/homekit/bin/relay_mqtt_http_proxy.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/sensors_bot.service b/systemd/sensors_bot.service index 50128b3..2470d92 100644 --- a/systemd/sensors_bot.service +++ b/systemd/sensors_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/sensors_bot.py +ExecStart=/home/user/homekit/bin/sensors_bot.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/sound_bot.service b/systemd/sound_bot.service index 51a9e0f..e0b5500 100644 --- a/systemd/sound_bot.service +++ b/systemd/sound_bot.service @@ -6,7 +6,7 @@ After=network-online.target Restart=on-failure User=user WorkingDirectory=/home/user -ExecStart=/home/user/homekit/src/sound_bot.py +ExecStart=/home/user/homekit/bin/sound_bot.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd/sound_node.service b/systemd/sound_node.service index e3e3afd..a14ec1f 100644 --- a/systemd/sound_node.service +++ b/systemd/sound_node.service @@ -6,7 +6,7 @@ After=network-online.target User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/sound_node.py --config /etc/sound_node.toml +ExecStart=/home/user/homekit/bin/sound_node.py --config /etc/sound_node.toml WorkingDirectory=/root [Install] diff --git a/systemd/sound_sensor_node.service b/systemd/sound_sensor_node.service index d10f976..dfc2ecd 100644 --- a/systemd/sound_sensor_node.service +++ b/systemd/sound_sensor_node.service @@ -6,7 +6,7 @@ After=network-online.target User=root Group=root Restart=on-failure -ExecStart=/home/user/homekit/src/sound_sensor_node.py --config /etc/sound_sensor_node.toml +ExecStart=/home/user/homekit/bin/sound_sensor_node.py --config /etc/sound_sensor_node.toml WorkingDirectory=/root [Install] diff --git a/systemd/sound_sensor_server.service b/systemd/sound_sensor_server.service index 0133e53..5ab08cd 100644 --- a/systemd/sound_sensor_server.service +++ b/systemd/sound_sensor_server.service @@ -6,7 +6,7 @@ After=network-online.target User=user Group=user Restart=on-failure -ExecStart=/home/user/homekit/src/sound_sensor_server.py +ExecStart=/home/user/homekit/bin/sound_sensor_server.py WorkingDirectory=/home/user [Install] diff --git a/systemd/temphumd.service b/systemd/temphumd.service index 1da9617..dd5ec55 100644 --- a/systemd/temphumd.service +++ b/systemd/temphumd.service @@ -4,7 +4,7 @@ After=network-online.target [Service] Restart=on-failure -ExecStart=/home/user/homekit/src/temphumd.py --config /etc/temphumd.toml +ExecStart=/home/user/homekit/bin/temphumd.py --config /etc/temphumd.toml [Install] WantedBy=multi-user.target diff --git a/systemd/temphumd@.service b/systemd/temphumd@.service index d1c840d..7b1b11e 100644 --- a/systemd/temphumd@.service +++ b/systemd/temphumd@.service @@ -4,7 +4,7 @@ After=network-online.target [Service] Restart=on-failure -ExecStart=/home/user/homekit/src/temphumd.py --config /etc/temphumd-%i.toml +ExecStart=/home/user/homekit/bin/temphumd.py --config /etc/temphumd-%i.toml [Install] WantedBy=multi-user.target diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/test.py b/test/test.py index 7ea37e6..413c25c 100755 --- a/test/test.py +++ b/test/test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -from home.relay import RelayClient +from homekit.relay import RelayClient if __name__ == '__main__': diff --git a/test/test_stopwatch.py b/test/test_stopwatch.py index 6ff2c0e..9dd7762 100755 --- a/test/test_stopwatch.py +++ b/test/test_stopwatch.py @@ -1,4 +1,4 @@ -from home.util import Stopwatch, StopwatchError +from homekit.util import Stopwatch, StopwatchError from time import sleep From a6d8ba93056c1a4e243d56da447e241b2504fae2 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 23:20:37 +0300 Subject: [PATCH 07/51] move files again --- .gitignore | 12 +++++------ .../CameraWebServer/CameraWebServer.ino | 0 .../esp32-cam}/CameraWebServer/app_httpd.cpp | 0 .../esp32-cam}/CameraWebServer/camera_index.h | 0 .../esp32-cam}/CameraWebServer/camera_pins.h | 0 .../CameraWebServer/index_ov2640.html | 0 bin/__py_include.py | 2 +- .../pio}/include/homekit/logging.h | 0 .../pio}/include/homekit/macros.h | 0 .../pio}/include/homekit/stopwatch.h | 0 .../pio}/include/homekit/util.h | 0 .../pio}/libs/config/homekit/config.cpp | 0 .../pio}/libs/config/homekit/config.h | 0 .../pio}/libs/config/library.json | 0 .../libs/http_server/homekit/http_server.cpp | 0 .../libs/http_server/homekit/http_server.h | 0 .../pio}/libs/http_server/library.json | 0 .../pio}/libs/led/homekit/led.cpp | 0 .../pio}/libs/led/homekit/led.h | 0 .../pio}/libs/led/library.json | 0 .../pio}/libs/main/homekit/main.cpp | 0 .../pio}/libs/main/homekit/main.h | 0 .../pio}/libs/main/library.json | 0 .../pio}/libs/mqtt/homekit/mqtt/module.cpp | 0 .../pio}/libs/mqtt/homekit/mqtt/module.h | 0 .../pio}/libs/mqtt/homekit/mqtt/mqtt.cpp | 0 .../pio}/libs/mqtt/homekit/mqtt/mqtt.h | 0 .../pio}/libs/mqtt/homekit/mqtt/payload.h | 0 .../pio}/libs/mqtt/library.json | 0 .../homekit/mqtt/module/diagnostics.cpp | 0 .../homekit/mqtt/module/diagnostics.h | 0 .../libs/mqtt_module_diagnostics/library.json | 0 .../homekit/mqtt/module/ota.cpp | 0 .../mqtt_module_ota/homekit/mqtt/module/ota.h | 0 .../pio}/libs/mqtt_module_ota/library.json | 0 .../homekit/mqtt/module/relay.cpp | 0 .../homekit/mqtt/module/relay.h | 0 .../pio}/libs/mqtt_module_relay/library.json | 0 .../homekit/mqtt/module/temphum.cpp | 0 .../homekit/mqtt/module/temphum.h | 0 .../libs/mqtt_module_temphum/library.json | 0 .../pio}/libs/relay/homekit/relay.cpp | 0 .../pio}/libs/relay/homekit/relay.h | 0 .../pio}/libs/relay/library.json | 0 .../pio}/libs/static/homekit/static.cpp | 0 .../pio}/libs/static/homekit/static.h | 0 .../pio}/libs/static/library.json | 0 .../pio}/libs/temphum/homekit/temphum.cpp | 0 .../pio}/libs/temphum/homekit/temphum.h | 0 .../pio}/libs/temphum/library.json | 0 .../pio}/libs/wifi/homekit/wifi.cpp | 0 .../pio}/libs/wifi/homekit/wifi.h | 0 .../pio}/libs/wifi/library.json | 0 .../common => include/pio}/make_static.sh | 0 .../common => include/pio}/static/app.js | 0 .../common => include/pio}/static/favicon.ico | Bin .../common => include/pio}/static/index.html | 0 .../common => include/pio}/static/md5.js | 0 .../common => include/pio}/static/style.css | 0 {py_include => include/py}/__init__.py | 0 .../py}/homekit/__init__.py | 0 .../py}/homekit/api/__init__.py | 0 .../py}/homekit/api/__init__.pyi | 0 .../py}/homekit/api/config.py | 0 .../py}/homekit/api/errors/__init__.py | 0 .../homekit/api/errors/api_response_error.py | 0 .../py}/homekit/api/types/__init__.py | 0 .../py}/homekit/api/types/types.py | 0 .../py}/homekit/api/web_api_client.py | 0 .../py}/homekit/audio/__init__.py | 0 .../py}/homekit/audio/amixer.py | 0 .../py}/homekit/camera/__init__.py | 0 .../py}/homekit/camera/esp32.py | 0 .../py}/homekit/camera/types.py | 0 .../py}/homekit/camera/util.py | 0 .../py}/homekit/config/__init__.py | 0 .../py}/homekit/config/_configs.py | 0 .../py}/homekit/config/config.py | 0 .../py}/homekit/database/__init__.py | 0 .../py}/homekit/database/__init__.pyi | 0 .../py}/homekit/database/_base.py | 0 .../py}/homekit/database/bots.py | 0 .../py}/homekit/database/clickhouse.py | 0 .../py}/homekit/database/inverter.py | 0 .../homekit/database/inverter_time_formats.py | 0 .../py}/homekit/database/mysql.py | 0 .../py}/homekit/database/sensors.py | 0 .../py}/homekit/database/simple_state.py | 0 .../py}/homekit/database/sqlite.py | 0 .../py}/homekit/http/__init__.py | 0 .../py}/homekit/http/http.py | 0 .../py}/homekit/inverter/__init__.py | 0 .../py}/homekit/inverter/config.py | 0 .../py}/homekit/inverter/emulator.py | 0 .../py}/homekit/inverter/inverter_wrapper.py | 0 .../py}/homekit/inverter/monitor.py | 0 .../py}/homekit/inverter/types.py | 0 .../py}/homekit/inverter/util.py | 0 .../py}/homekit/media/__init__.py | 0 .../py}/homekit/media/__init__.pyi | 0 .../py}/homekit/media/node_client.py | 0 .../py}/homekit/media/node_server.py | 0 .../py}/homekit/media/record.py | 0 .../py}/homekit/media/record_client.py | 0 .../py}/homekit/media/storage.py | 0 .../py}/homekit/media/types.py | 0 .../py}/homekit/mqtt/__init__.py | 0 .../py}/homekit/mqtt/_config.py | 0 .../py}/homekit/mqtt/_module.py | 0 .../py}/homekit/mqtt/_mqtt.py | 0 .../py}/homekit/mqtt/_node.py | 0 .../py}/homekit/mqtt/_payload.py | 0 .../py}/homekit/mqtt/_util.py | 0 .../py}/homekit/mqtt/_wrapper.py | 0 .../py}/homekit/mqtt/module/diagnostics.py | 0 .../py}/homekit/mqtt/module/inverter.py | 0 .../py}/homekit/mqtt/module/ota.py | 0 .../py}/homekit/mqtt/module/relay.py | 0 .../py}/homekit/mqtt/module/temphum.py | 0 .../py}/homekit/pio/__init__.py | 0 .../py}/homekit/pio/exceptions.py | 0 .../py}/homekit/pio/products.py | 0 .../py}/homekit/relay/__init__.py | 0 .../py}/homekit/relay/__init__.pyi | 0 .../py}/homekit/relay/sunxi_h3_client.py | 0 .../py}/homekit/relay/sunxi_h3_server.py | 0 .../py}/homekit/soundsensor/__init__.py | 0 .../py}/homekit/soundsensor/__init__.pyi | 0 .../py}/homekit/soundsensor/node.py | 0 .../py}/homekit/soundsensor/server.py | 0 .../py}/homekit/soundsensor/server_client.py | 0 .../py}/homekit/telegram/__init__.py | 0 .../py}/homekit/telegram/_botcontext.py | 0 .../py}/homekit/telegram/_botdb.py | 0 .../py}/homekit/telegram/_botlang.py | 0 .../py}/homekit/telegram/_botutil.py | 0 .../py}/homekit/telegram/aio.py | 0 .../py}/homekit/telegram/bot.py | 0 .../py}/homekit/telegram/config.py | 0 .../py}/homekit/telegram/telegram.py | 0 .../py}/homekit/temphum/__init__.py | 0 .../py}/homekit/temphum/base.py | 0 .../py}/homekit/temphum/i2c.py | 0 {py_include => include/py}/homekit/util.py | 0 {py_include => include/py}/pyA20/__init__.pyi | 0 .../py}/pyA20/gpio/connector.pyi | 0 .../py}/pyA20/gpio/gpio.pyi | 0 .../py}/pyA20/gpio/port.pyi | 0 {py_include => include/py}/pyA20/port.pyi | 0 .../py}/syncleo/__init__.py | 0 {py_include => include/py}/syncleo/kettle.py | 0 .../py}/syncleo/protocol.py | 0 {platformio => pio}/dumb_mqtt/src/main.cpp | 0 {platformio => pio}/relayctl/src/main.cpp | 0 {platformio => pio}/temphum/src/main.cpp | 0 .../temphum_relayctl/src/main.cpp | 0 platformio/dumb_mqtt/.gitignore | 3 --- platformio/relayctl/.gitignore | 3 --- platformio/temphum/.gitignore | 3 --- test/__py_include.py | 9 +++++++++ test/mqtt_relay_server_util.py | 18 ++++++----------- test/mqtt_relay_util.py | 12 +++-------- test/test.py | 1 + test/test_amixer.py | 9 +++------ test/test_api.py | 14 ++++--------- test/test_esp32_cam.py | 14 ++++--------- test/test_inverter_monitor.py | 19 ++++-------------- test/test_ipcam_server_cleanup.py | 16 +++++---------- test/test_polaris_stuff.py | 11 ++-------- test/test_record_upload.py | 17 +++++----------- test/test_send_fake_sound_hit.py | 10 ++------- test/test_sensors_plot.py | 0 test/test_sound_node_client.py | 9 +++------ test/test_sound_server_api.py | 14 ++++--------- test/test_stopwatch.py | 1 + test/test_telegram_aio_send_photo.py | 13 +++--------- 176 files changed, 66 insertions(+), 144 deletions(-) rename {esp32-cam => arduino/esp32-cam}/CameraWebServer/CameraWebServer.ino (100%) rename {esp32-cam => arduino/esp32-cam}/CameraWebServer/app_httpd.cpp (100%) rename {esp32-cam => arduino/esp32-cam}/CameraWebServer/camera_index.h (100%) rename {esp32-cam => arduino/esp32-cam}/CameraWebServer/camera_pins.h (100%) rename {esp32-cam => arduino/esp32-cam}/CameraWebServer/index_ov2640.html (100%) rename {platformio/common => include/pio}/include/homekit/logging.h (100%) rename {platformio/common => include/pio}/include/homekit/macros.h (100%) rename {platformio/common => include/pio}/include/homekit/stopwatch.h (100%) rename {platformio/common => include/pio}/include/homekit/util.h (100%) rename {platformio/common => include/pio}/libs/config/homekit/config.cpp (100%) rename {platformio/common => include/pio}/libs/config/homekit/config.h (100%) rename {platformio/common => include/pio}/libs/config/library.json (100%) rename {platformio/common => include/pio}/libs/http_server/homekit/http_server.cpp (100%) rename {platformio/common => include/pio}/libs/http_server/homekit/http_server.h (100%) rename {platformio/common => include/pio}/libs/http_server/library.json (100%) rename {platformio/common => include/pio}/libs/led/homekit/led.cpp (100%) rename {platformio/common => include/pio}/libs/led/homekit/led.h (100%) rename {platformio/common => include/pio}/libs/led/library.json (100%) rename {platformio/common => include/pio}/libs/main/homekit/main.cpp (100%) rename {platformio/common => include/pio}/libs/main/homekit/main.h (100%) rename {platformio/common => include/pio}/libs/main/library.json (100%) rename {platformio/common => include/pio}/libs/mqtt/homekit/mqtt/module.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt/homekit/mqtt/module.h (100%) rename {platformio/common => include/pio}/libs/mqtt/homekit/mqtt/mqtt.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt/homekit/mqtt/mqtt.h (100%) rename {platformio/common => include/pio}/libs/mqtt/homekit/mqtt/payload.h (100%) rename {platformio/common => include/pio}/libs/mqtt/library.json (100%) rename {platformio/common => include/pio}/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h (100%) rename {platformio/common => include/pio}/libs/mqtt_module_diagnostics/library.json (100%) rename {platformio/common => include/pio}/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt_module_ota/homekit/mqtt/module/ota.h (100%) rename {platformio/common => include/pio}/libs/mqtt_module_ota/library.json (100%) rename {platformio/common => include/pio}/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt_module_relay/homekit/mqtt/module/relay.h (100%) rename {platformio/common => include/pio}/libs/mqtt_module_relay/library.json (100%) rename {platformio/common => include/pio}/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp (100%) rename {platformio/common => include/pio}/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h (100%) rename {platformio/common => include/pio}/libs/mqtt_module_temphum/library.json (100%) rename {platformio/common => include/pio}/libs/relay/homekit/relay.cpp (100%) rename {platformio/common => include/pio}/libs/relay/homekit/relay.h (100%) rename {platformio/common => include/pio}/libs/relay/library.json (100%) rename {platformio/common => include/pio}/libs/static/homekit/static.cpp (100%) rename {platformio/common => include/pio}/libs/static/homekit/static.h (100%) rename {platformio/common => include/pio}/libs/static/library.json (100%) rename {platformio/common => include/pio}/libs/temphum/homekit/temphum.cpp (100%) rename {platformio/common => include/pio}/libs/temphum/homekit/temphum.h (100%) rename {platformio/common => include/pio}/libs/temphum/library.json (100%) rename {platformio/common => include/pio}/libs/wifi/homekit/wifi.cpp (100%) rename {platformio/common => include/pio}/libs/wifi/homekit/wifi.h (100%) rename {platformio/common => include/pio}/libs/wifi/library.json (100%) rename {platformio/common => include/pio}/make_static.sh (100%) rename {platformio/common => include/pio}/static/app.js (100%) rename {platformio/common => include/pio}/static/favicon.ico (100%) rename {platformio/common => include/pio}/static/index.html (100%) rename {platformio/common => include/pio}/static/md5.js (100%) rename {platformio/common => include/pio}/static/style.css (100%) rename {py_include => include/py}/__init__.py (100%) rename {py_include => include/py}/homekit/__init__.py (100%) rename {py_include => include/py}/homekit/api/__init__.py (100%) rename {py_include => include/py}/homekit/api/__init__.pyi (100%) rename {py_include => include/py}/homekit/api/config.py (100%) rename {py_include => include/py}/homekit/api/errors/__init__.py (100%) rename {py_include => include/py}/homekit/api/errors/api_response_error.py (100%) rename {py_include => include/py}/homekit/api/types/__init__.py (100%) rename {py_include => include/py}/homekit/api/types/types.py (100%) rename {py_include => include/py}/homekit/api/web_api_client.py (100%) rename {py_include => include/py}/homekit/audio/__init__.py (100%) rename {py_include => include/py}/homekit/audio/amixer.py (100%) rename {py_include => include/py}/homekit/camera/__init__.py (100%) rename {py_include => include/py}/homekit/camera/esp32.py (100%) rename {py_include => include/py}/homekit/camera/types.py (100%) rename {py_include => include/py}/homekit/camera/util.py (100%) rename {py_include => include/py}/homekit/config/__init__.py (100%) rename {py_include => include/py}/homekit/config/_configs.py (100%) rename {py_include => include/py}/homekit/config/config.py (100%) rename {py_include => include/py}/homekit/database/__init__.py (100%) rename {py_include => include/py}/homekit/database/__init__.pyi (100%) rename {py_include => include/py}/homekit/database/_base.py (100%) rename {py_include => include/py}/homekit/database/bots.py (100%) rename {py_include => include/py}/homekit/database/clickhouse.py (100%) rename {py_include => include/py}/homekit/database/inverter.py (100%) rename {py_include => include/py}/homekit/database/inverter_time_formats.py (100%) rename {py_include => include/py}/homekit/database/mysql.py (100%) rename {py_include => include/py}/homekit/database/sensors.py (100%) rename {py_include => include/py}/homekit/database/simple_state.py (100%) rename {py_include => include/py}/homekit/database/sqlite.py (100%) rename {py_include => include/py}/homekit/http/__init__.py (100%) rename {py_include => include/py}/homekit/http/http.py (100%) rename {py_include => include/py}/homekit/inverter/__init__.py (100%) rename {py_include => include/py}/homekit/inverter/config.py (100%) rename {py_include => include/py}/homekit/inverter/emulator.py (100%) rename {py_include => include/py}/homekit/inverter/inverter_wrapper.py (100%) rename {py_include => include/py}/homekit/inverter/monitor.py (100%) rename {py_include => include/py}/homekit/inverter/types.py (100%) rename {py_include => include/py}/homekit/inverter/util.py (100%) rename {py_include => include/py}/homekit/media/__init__.py (100%) rename {py_include => include/py}/homekit/media/__init__.pyi (100%) rename {py_include => include/py}/homekit/media/node_client.py (100%) rename {py_include => include/py}/homekit/media/node_server.py (100%) rename {py_include => include/py}/homekit/media/record.py (100%) rename {py_include => include/py}/homekit/media/record_client.py (100%) rename {py_include => include/py}/homekit/media/storage.py (100%) rename {py_include => include/py}/homekit/media/types.py (100%) rename {py_include => include/py}/homekit/mqtt/__init__.py (100%) rename {py_include => include/py}/homekit/mqtt/_config.py (100%) rename {py_include => include/py}/homekit/mqtt/_module.py (100%) rename {py_include => include/py}/homekit/mqtt/_mqtt.py (100%) rename {py_include => include/py}/homekit/mqtt/_node.py (100%) rename {py_include => include/py}/homekit/mqtt/_payload.py (100%) rename {py_include => include/py}/homekit/mqtt/_util.py (100%) rename {py_include => include/py}/homekit/mqtt/_wrapper.py (100%) rename {py_include => include/py}/homekit/mqtt/module/diagnostics.py (100%) rename {py_include => include/py}/homekit/mqtt/module/inverter.py (100%) rename {py_include => include/py}/homekit/mqtt/module/ota.py (100%) rename {py_include => include/py}/homekit/mqtt/module/relay.py (100%) rename {py_include => include/py}/homekit/mqtt/module/temphum.py (100%) rename {py_include => include/py}/homekit/pio/__init__.py (100%) rename {py_include => include/py}/homekit/pio/exceptions.py (100%) rename {py_include => include/py}/homekit/pio/products.py (100%) rename {py_include => include/py}/homekit/relay/__init__.py (100%) rename {py_include => include/py}/homekit/relay/__init__.pyi (100%) rename {py_include => include/py}/homekit/relay/sunxi_h3_client.py (100%) rename {py_include => include/py}/homekit/relay/sunxi_h3_server.py (100%) rename {py_include => include/py}/homekit/soundsensor/__init__.py (100%) rename {py_include => include/py}/homekit/soundsensor/__init__.pyi (100%) rename {py_include => include/py}/homekit/soundsensor/node.py (100%) rename {py_include => include/py}/homekit/soundsensor/server.py (100%) rename {py_include => include/py}/homekit/soundsensor/server_client.py (100%) rename {py_include => include/py}/homekit/telegram/__init__.py (100%) rename {py_include => include/py}/homekit/telegram/_botcontext.py (100%) rename {py_include => include/py}/homekit/telegram/_botdb.py (100%) rename {py_include => include/py}/homekit/telegram/_botlang.py (100%) rename {py_include => include/py}/homekit/telegram/_botutil.py (100%) rename {py_include => include/py}/homekit/telegram/aio.py (100%) rename {py_include => include/py}/homekit/telegram/bot.py (100%) rename {py_include => include/py}/homekit/telegram/config.py (100%) rename {py_include => include/py}/homekit/telegram/telegram.py (100%) rename {py_include => include/py}/homekit/temphum/__init__.py (100%) rename {py_include => include/py}/homekit/temphum/base.py (100%) rename {py_include => include/py}/homekit/temphum/i2c.py (100%) rename {py_include => include/py}/homekit/util.py (100%) rename {py_include => include/py}/pyA20/__init__.pyi (100%) rename {py_include => include/py}/pyA20/gpio/connector.pyi (100%) rename {py_include => include/py}/pyA20/gpio/gpio.pyi (100%) rename {py_include => include/py}/pyA20/gpio/port.pyi (100%) rename {py_include => include/py}/pyA20/port.pyi (100%) rename {py_include => include/py}/syncleo/__init__.py (100%) rename {py_include => include/py}/syncleo/kettle.py (100%) rename {py_include => include/py}/syncleo/protocol.py (100%) rename {platformio => pio}/dumb_mqtt/src/main.cpp (100%) rename {platformio => pio}/relayctl/src/main.cpp (100%) rename {platformio => pio}/temphum/src/main.cpp (100%) rename {platformio => pio}/temphum_relayctl/src/main.cpp (100%) delete mode 100644 platformio/dumb_mqtt/.gitignore delete mode 100644 platformio/relayctl/.gitignore delete mode 100644 platformio/temphum/.gitignore create mode 100644 test/__py_include.py delete mode 100755 test/test_sensors_plot.py diff --git a/.gitignore b/.gitignore index 1280ea2..6de5e71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,19 +6,19 @@ config.def.h __pycache__ .DS_Store -/py_include/test/test_inverter_monitor.log +/include/test/test_inverter_monitor.log /youtrack-certificate /cpp -/py_include/test.py +/include/test.py /bin/test.py -/esp32-cam/CameraWebServer/wifi_password.h +/arduino/esp32-cam/CameraWebServer/wifi_password.h cmake-build-* .pio platformio.ini CMakeListsPrivate.txt -/platformio/*/CMakeLists.txt -/platformio/*/CMakeListsPrivate.txt -/platformio/*/.gitignore +/pio/*/CMakeLists.txt +/pio/*/CMakeListsPrivate.txt +/pio/*/.gitignore *.swp /localwebsite/vendor diff --git a/esp32-cam/CameraWebServer/CameraWebServer.ino b/arduino/esp32-cam/CameraWebServer/CameraWebServer.ino similarity index 100% rename from esp32-cam/CameraWebServer/CameraWebServer.ino rename to arduino/esp32-cam/CameraWebServer/CameraWebServer.ino diff --git a/esp32-cam/CameraWebServer/app_httpd.cpp b/arduino/esp32-cam/CameraWebServer/app_httpd.cpp similarity index 100% rename from esp32-cam/CameraWebServer/app_httpd.cpp rename to arduino/esp32-cam/CameraWebServer/app_httpd.cpp diff --git a/esp32-cam/CameraWebServer/camera_index.h b/arduino/esp32-cam/CameraWebServer/camera_index.h similarity index 100% rename from esp32-cam/CameraWebServer/camera_index.h rename to arduino/esp32-cam/CameraWebServer/camera_index.h diff --git a/esp32-cam/CameraWebServer/camera_pins.h b/arduino/esp32-cam/CameraWebServer/camera_pins.h similarity index 100% rename from esp32-cam/CameraWebServer/camera_pins.h rename to arduino/esp32-cam/CameraWebServer/camera_pins.h diff --git a/esp32-cam/CameraWebServer/index_ov2640.html b/arduino/esp32-cam/CameraWebServer/index_ov2640.html similarity index 100% rename from esp32-cam/CameraWebServer/index_ov2640.html rename to arduino/esp32-cam/CameraWebServer/index_ov2640.html diff --git a/bin/__py_include.py b/bin/__py_include.py index 7f95e28..8f98830 100644 --- a/bin/__py_include.py +++ b/bin/__py_include.py @@ -1,7 +1,7 @@ import sys import os.path -for _name in ('py_include',): +for _name in ('include/py',): sys.path.extend([ os.path.realpath( os.path.join(os.path.dirname(os.path.join(__file__)), '..', _name) diff --git a/platformio/common/include/homekit/logging.h b/include/pio/include/homekit/logging.h similarity index 100% rename from platformio/common/include/homekit/logging.h rename to include/pio/include/homekit/logging.h diff --git a/platformio/common/include/homekit/macros.h b/include/pio/include/homekit/macros.h similarity index 100% rename from platformio/common/include/homekit/macros.h rename to include/pio/include/homekit/macros.h diff --git a/platformio/common/include/homekit/stopwatch.h b/include/pio/include/homekit/stopwatch.h similarity index 100% rename from platformio/common/include/homekit/stopwatch.h rename to include/pio/include/homekit/stopwatch.h diff --git a/platformio/common/include/homekit/util.h b/include/pio/include/homekit/util.h similarity index 100% rename from platformio/common/include/homekit/util.h rename to include/pio/include/homekit/util.h diff --git a/platformio/common/libs/config/homekit/config.cpp b/include/pio/libs/config/homekit/config.cpp similarity index 100% rename from platformio/common/libs/config/homekit/config.cpp rename to 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 similarity index 100% rename from platformio/common/libs/config/homekit/config.h rename to include/pio/libs/config/homekit/config.h diff --git a/platformio/common/libs/config/library.json b/include/pio/libs/config/library.json similarity index 100% rename from platformio/common/libs/config/library.json rename to 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 similarity index 100% rename from platformio/common/libs/http_server/homekit/http_server.cpp rename to 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 similarity index 100% rename from platformio/common/libs/http_server/homekit/http_server.h rename to 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 similarity index 100% rename from platformio/common/libs/http_server/library.json rename to 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 similarity index 100% rename from platformio/common/libs/led/homekit/led.cpp rename to 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 similarity index 100% rename from platformio/common/libs/led/homekit/led.h rename to include/pio/libs/led/homekit/led.h diff --git a/platformio/common/libs/led/library.json b/include/pio/libs/led/library.json similarity index 100% rename from platformio/common/libs/led/library.json rename to include/pio/libs/led/library.json diff --git a/platformio/common/libs/main/homekit/main.cpp b/include/pio/libs/main/homekit/main.cpp similarity index 100% rename from platformio/common/libs/main/homekit/main.cpp rename to include/pio/libs/main/homekit/main.cpp diff --git a/platformio/common/libs/main/homekit/main.h b/include/pio/libs/main/homekit/main.h similarity index 100% rename from platformio/common/libs/main/homekit/main.h rename to include/pio/libs/main/homekit/main.h diff --git a/platformio/common/libs/main/library.json b/include/pio/libs/main/library.json similarity index 100% rename from platformio/common/libs/main/library.json rename to include/pio/libs/main/library.json diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.cpp b/include/pio/libs/mqtt/homekit/mqtt/module.cpp similarity index 100% rename from platformio/common/libs/mqtt/homekit/mqtt/module.cpp rename to include/pio/libs/mqtt/homekit/mqtt/module.cpp diff --git a/platformio/common/libs/mqtt/homekit/mqtt/module.h b/include/pio/libs/mqtt/homekit/mqtt/module.h similarity index 100% rename from platformio/common/libs/mqtt/homekit/mqtt/module.h rename to include/pio/libs/mqtt/homekit/mqtt/module.h diff --git a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp similarity index 100% rename from platformio/common/libs/mqtt/homekit/mqtt/mqtt.cpp rename to include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp diff --git a/platformio/common/libs/mqtt/homekit/mqtt/mqtt.h b/include/pio/libs/mqtt/homekit/mqtt/mqtt.h similarity index 100% rename from platformio/common/libs/mqtt/homekit/mqtt/mqtt.h rename to 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 similarity index 100% rename from platformio/common/libs/mqtt/homekit/mqtt/payload.h rename to include/pio/libs/mqtt/homekit/mqtt/payload.h diff --git a/platformio/common/libs/mqtt/library.json b/include/pio/libs/mqtt/library.json similarity index 100% rename from platformio/common/libs/mqtt/library.json rename to include/pio/libs/mqtt/library.json 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 similarity index 100% rename from platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp rename to include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp diff --git a/platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h similarity index 100% rename from platformio/common/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h rename to include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h diff --git a/platformio/common/libs/mqtt_module_diagnostics/library.json b/include/pio/libs/mqtt_module_diagnostics/library.json similarity index 100% rename from platformio/common/libs/mqtt_module_diagnostics/library.json rename to include/pio/libs/mqtt_module_diagnostics/library.json 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 similarity index 100% rename from platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp rename to include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp diff --git a/platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.h b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h similarity index 100% rename from platformio/common/libs/mqtt_module_ota/homekit/mqtt/module/ota.h rename to include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h diff --git a/platformio/common/libs/mqtt_module_ota/library.json b/include/pio/libs/mqtt_module_ota/library.json similarity index 100% rename from platformio/common/libs/mqtt_module_ota/library.json rename to include/pio/libs/mqtt_module_ota/library.json 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 similarity index 100% rename from platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp rename to include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp diff --git a/platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h similarity index 100% rename from platformio/common/libs/mqtt_module_relay/homekit/mqtt/module/relay.h rename to include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h diff --git a/platformio/common/libs/mqtt_module_relay/library.json b/include/pio/libs/mqtt_module_relay/library.json similarity index 100% rename from platformio/common/libs/mqtt_module_relay/library.json rename to include/pio/libs/mqtt_module_relay/library.json 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 similarity index 100% rename from platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp rename to include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp diff --git a/platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h similarity index 100% rename from platformio/common/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h rename to include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h diff --git a/platformio/common/libs/mqtt_module_temphum/library.json b/include/pio/libs/mqtt_module_temphum/library.json similarity index 100% rename from platformio/common/libs/mqtt_module_temphum/library.json rename to include/pio/libs/mqtt_module_temphum/library.json diff --git a/platformio/common/libs/relay/homekit/relay.cpp b/include/pio/libs/relay/homekit/relay.cpp similarity index 100% rename from platformio/common/libs/relay/homekit/relay.cpp rename to 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 similarity index 100% rename from platformio/common/libs/relay/homekit/relay.h rename to include/pio/libs/relay/homekit/relay.h diff --git a/platformio/common/libs/relay/library.json b/include/pio/libs/relay/library.json similarity index 100% rename from platformio/common/libs/relay/library.json rename to include/pio/libs/relay/library.json diff --git a/platformio/common/libs/static/homekit/static.cpp b/include/pio/libs/static/homekit/static.cpp similarity index 100% rename from platformio/common/libs/static/homekit/static.cpp rename to 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 similarity index 100% rename from platformio/common/libs/static/homekit/static.h rename to include/pio/libs/static/homekit/static.h diff --git a/platformio/common/libs/static/library.json b/include/pio/libs/static/library.json similarity index 100% rename from platformio/common/libs/static/library.json rename to include/pio/libs/static/library.json diff --git a/platformio/common/libs/temphum/homekit/temphum.cpp b/include/pio/libs/temphum/homekit/temphum.cpp similarity index 100% rename from platformio/common/libs/temphum/homekit/temphum.cpp rename to 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 similarity index 100% rename from platformio/common/libs/temphum/homekit/temphum.h rename to include/pio/libs/temphum/homekit/temphum.h diff --git a/platformio/common/libs/temphum/library.json b/include/pio/libs/temphum/library.json similarity index 100% rename from platformio/common/libs/temphum/library.json rename to include/pio/libs/temphum/library.json diff --git a/platformio/common/libs/wifi/homekit/wifi.cpp b/include/pio/libs/wifi/homekit/wifi.cpp similarity index 100% rename from platformio/common/libs/wifi/homekit/wifi.cpp rename to 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 similarity index 100% rename from platformio/common/libs/wifi/homekit/wifi.h rename to include/pio/libs/wifi/homekit/wifi.h diff --git a/platformio/common/libs/wifi/library.json b/include/pio/libs/wifi/library.json similarity index 100% rename from platformio/common/libs/wifi/library.json rename to include/pio/libs/wifi/library.json diff --git a/platformio/common/make_static.sh b/include/pio/make_static.sh similarity index 100% rename from platformio/common/make_static.sh rename to include/pio/make_static.sh diff --git a/platformio/common/static/app.js b/include/pio/static/app.js similarity index 100% rename from platformio/common/static/app.js rename to include/pio/static/app.js diff --git a/platformio/common/static/favicon.ico b/include/pio/static/favicon.ico similarity index 100% rename from platformio/common/static/favicon.ico rename to include/pio/static/favicon.ico diff --git a/platformio/common/static/index.html b/include/pio/static/index.html similarity index 100% rename from platformio/common/static/index.html rename to include/pio/static/index.html diff --git a/platformio/common/static/md5.js b/include/pio/static/md5.js similarity index 100% rename from platformio/common/static/md5.js rename to include/pio/static/md5.js diff --git a/platformio/common/static/style.css b/include/pio/static/style.css similarity index 100% rename from platformio/common/static/style.css rename to include/pio/static/style.css diff --git a/py_include/__init__.py b/include/py/__init__.py similarity index 100% rename from py_include/__init__.py rename to include/py/__init__.py diff --git a/py_include/homekit/__init__.py b/include/py/homekit/__init__.py similarity index 100% rename from py_include/homekit/__init__.py rename to include/py/homekit/__init__.py diff --git a/py_include/homekit/api/__init__.py b/include/py/homekit/api/__init__.py similarity index 100% rename from py_include/homekit/api/__init__.py rename to include/py/homekit/api/__init__.py diff --git a/py_include/homekit/api/__init__.pyi b/include/py/homekit/api/__init__.pyi similarity index 100% rename from py_include/homekit/api/__init__.pyi rename to include/py/homekit/api/__init__.pyi diff --git a/py_include/homekit/api/config.py b/include/py/homekit/api/config.py similarity index 100% rename from py_include/homekit/api/config.py rename to include/py/homekit/api/config.py diff --git a/py_include/homekit/api/errors/__init__.py b/include/py/homekit/api/errors/__init__.py similarity index 100% rename from py_include/homekit/api/errors/__init__.py rename to include/py/homekit/api/errors/__init__.py diff --git a/py_include/homekit/api/errors/api_response_error.py b/include/py/homekit/api/errors/api_response_error.py similarity index 100% rename from py_include/homekit/api/errors/api_response_error.py rename to include/py/homekit/api/errors/api_response_error.py diff --git a/py_include/homekit/api/types/__init__.py b/include/py/homekit/api/types/__init__.py similarity index 100% rename from py_include/homekit/api/types/__init__.py rename to include/py/homekit/api/types/__init__.py diff --git a/py_include/homekit/api/types/types.py b/include/py/homekit/api/types/types.py similarity index 100% rename from py_include/homekit/api/types/types.py rename to include/py/homekit/api/types/types.py diff --git a/py_include/homekit/api/web_api_client.py b/include/py/homekit/api/web_api_client.py similarity index 100% rename from py_include/homekit/api/web_api_client.py rename to include/py/homekit/api/web_api_client.py diff --git a/py_include/homekit/audio/__init__.py b/include/py/homekit/audio/__init__.py similarity index 100% rename from py_include/homekit/audio/__init__.py rename to include/py/homekit/audio/__init__.py diff --git a/py_include/homekit/audio/amixer.py b/include/py/homekit/audio/amixer.py similarity index 100% rename from py_include/homekit/audio/amixer.py rename to include/py/homekit/audio/amixer.py diff --git a/py_include/homekit/camera/__init__.py b/include/py/homekit/camera/__init__.py similarity index 100% rename from py_include/homekit/camera/__init__.py rename to include/py/homekit/camera/__init__.py diff --git a/py_include/homekit/camera/esp32.py b/include/py/homekit/camera/esp32.py similarity index 100% rename from py_include/homekit/camera/esp32.py rename to include/py/homekit/camera/esp32.py diff --git a/py_include/homekit/camera/types.py b/include/py/homekit/camera/types.py similarity index 100% rename from py_include/homekit/camera/types.py rename to include/py/homekit/camera/types.py diff --git a/py_include/homekit/camera/util.py b/include/py/homekit/camera/util.py similarity index 100% rename from py_include/homekit/camera/util.py rename to include/py/homekit/camera/util.py diff --git a/py_include/homekit/config/__init__.py b/include/py/homekit/config/__init__.py similarity index 100% rename from py_include/homekit/config/__init__.py rename to include/py/homekit/config/__init__.py diff --git a/py_include/homekit/config/_configs.py b/include/py/homekit/config/_configs.py similarity index 100% rename from py_include/homekit/config/_configs.py rename to include/py/homekit/config/_configs.py diff --git a/py_include/homekit/config/config.py b/include/py/homekit/config/config.py similarity index 100% rename from py_include/homekit/config/config.py rename to include/py/homekit/config/config.py diff --git a/py_include/homekit/database/__init__.py b/include/py/homekit/database/__init__.py similarity index 100% rename from py_include/homekit/database/__init__.py rename to include/py/homekit/database/__init__.py diff --git a/py_include/homekit/database/__init__.pyi b/include/py/homekit/database/__init__.pyi similarity index 100% rename from py_include/homekit/database/__init__.pyi rename to include/py/homekit/database/__init__.pyi diff --git a/py_include/homekit/database/_base.py b/include/py/homekit/database/_base.py similarity index 100% rename from py_include/homekit/database/_base.py rename to include/py/homekit/database/_base.py diff --git a/py_include/homekit/database/bots.py b/include/py/homekit/database/bots.py similarity index 100% rename from py_include/homekit/database/bots.py rename to include/py/homekit/database/bots.py diff --git a/py_include/homekit/database/clickhouse.py b/include/py/homekit/database/clickhouse.py similarity index 100% rename from py_include/homekit/database/clickhouse.py rename to include/py/homekit/database/clickhouse.py diff --git a/py_include/homekit/database/inverter.py b/include/py/homekit/database/inverter.py similarity index 100% rename from py_include/homekit/database/inverter.py rename to include/py/homekit/database/inverter.py diff --git a/py_include/homekit/database/inverter_time_formats.py b/include/py/homekit/database/inverter_time_formats.py similarity index 100% rename from py_include/homekit/database/inverter_time_formats.py rename to include/py/homekit/database/inverter_time_formats.py diff --git a/py_include/homekit/database/mysql.py b/include/py/homekit/database/mysql.py similarity index 100% rename from py_include/homekit/database/mysql.py rename to include/py/homekit/database/mysql.py diff --git a/py_include/homekit/database/sensors.py b/include/py/homekit/database/sensors.py similarity index 100% rename from py_include/homekit/database/sensors.py rename to include/py/homekit/database/sensors.py diff --git a/py_include/homekit/database/simple_state.py b/include/py/homekit/database/simple_state.py similarity index 100% rename from py_include/homekit/database/simple_state.py rename to include/py/homekit/database/simple_state.py diff --git a/py_include/homekit/database/sqlite.py b/include/py/homekit/database/sqlite.py similarity index 100% rename from py_include/homekit/database/sqlite.py rename to include/py/homekit/database/sqlite.py diff --git a/py_include/homekit/http/__init__.py b/include/py/homekit/http/__init__.py similarity index 100% rename from py_include/homekit/http/__init__.py rename to include/py/homekit/http/__init__.py diff --git a/py_include/homekit/http/http.py b/include/py/homekit/http/http.py similarity index 100% rename from py_include/homekit/http/http.py rename to include/py/homekit/http/http.py diff --git a/py_include/homekit/inverter/__init__.py b/include/py/homekit/inverter/__init__.py similarity index 100% rename from py_include/homekit/inverter/__init__.py rename to include/py/homekit/inverter/__init__.py diff --git a/py_include/homekit/inverter/config.py b/include/py/homekit/inverter/config.py similarity index 100% rename from py_include/homekit/inverter/config.py rename to include/py/homekit/inverter/config.py diff --git a/py_include/homekit/inverter/emulator.py b/include/py/homekit/inverter/emulator.py similarity index 100% rename from py_include/homekit/inverter/emulator.py rename to include/py/homekit/inverter/emulator.py diff --git a/py_include/homekit/inverter/inverter_wrapper.py b/include/py/homekit/inverter/inverter_wrapper.py similarity index 100% rename from py_include/homekit/inverter/inverter_wrapper.py rename to include/py/homekit/inverter/inverter_wrapper.py diff --git a/py_include/homekit/inverter/monitor.py b/include/py/homekit/inverter/monitor.py similarity index 100% rename from py_include/homekit/inverter/monitor.py rename to include/py/homekit/inverter/monitor.py diff --git a/py_include/homekit/inverter/types.py b/include/py/homekit/inverter/types.py similarity index 100% rename from py_include/homekit/inverter/types.py rename to include/py/homekit/inverter/types.py diff --git a/py_include/homekit/inverter/util.py b/include/py/homekit/inverter/util.py similarity index 100% rename from py_include/homekit/inverter/util.py rename to include/py/homekit/inverter/util.py diff --git a/py_include/homekit/media/__init__.py b/include/py/homekit/media/__init__.py similarity index 100% rename from py_include/homekit/media/__init__.py rename to include/py/homekit/media/__init__.py diff --git a/py_include/homekit/media/__init__.pyi b/include/py/homekit/media/__init__.pyi similarity index 100% rename from py_include/homekit/media/__init__.pyi rename to include/py/homekit/media/__init__.pyi diff --git a/py_include/homekit/media/node_client.py b/include/py/homekit/media/node_client.py similarity index 100% rename from py_include/homekit/media/node_client.py rename to include/py/homekit/media/node_client.py diff --git a/py_include/homekit/media/node_server.py b/include/py/homekit/media/node_server.py similarity index 100% rename from py_include/homekit/media/node_server.py rename to include/py/homekit/media/node_server.py diff --git a/py_include/homekit/media/record.py b/include/py/homekit/media/record.py similarity index 100% rename from py_include/homekit/media/record.py rename to include/py/homekit/media/record.py diff --git a/py_include/homekit/media/record_client.py b/include/py/homekit/media/record_client.py similarity index 100% rename from py_include/homekit/media/record_client.py rename to include/py/homekit/media/record_client.py diff --git a/py_include/homekit/media/storage.py b/include/py/homekit/media/storage.py similarity index 100% rename from py_include/homekit/media/storage.py rename to include/py/homekit/media/storage.py diff --git a/py_include/homekit/media/types.py b/include/py/homekit/media/types.py similarity index 100% rename from py_include/homekit/media/types.py rename to include/py/homekit/media/types.py diff --git a/py_include/homekit/mqtt/__init__.py b/include/py/homekit/mqtt/__init__.py similarity index 100% rename from py_include/homekit/mqtt/__init__.py rename to include/py/homekit/mqtt/__init__.py diff --git a/py_include/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py similarity index 100% rename from py_include/homekit/mqtt/_config.py rename to include/py/homekit/mqtt/_config.py diff --git a/py_include/homekit/mqtt/_module.py b/include/py/homekit/mqtt/_module.py similarity index 100% rename from py_include/homekit/mqtt/_module.py rename to include/py/homekit/mqtt/_module.py diff --git a/py_include/homekit/mqtt/_mqtt.py b/include/py/homekit/mqtt/_mqtt.py similarity index 100% rename from py_include/homekit/mqtt/_mqtt.py rename to include/py/homekit/mqtt/_mqtt.py diff --git a/py_include/homekit/mqtt/_node.py b/include/py/homekit/mqtt/_node.py similarity index 100% rename from py_include/homekit/mqtt/_node.py rename to include/py/homekit/mqtt/_node.py diff --git a/py_include/homekit/mqtt/_payload.py b/include/py/homekit/mqtt/_payload.py similarity index 100% rename from py_include/homekit/mqtt/_payload.py rename to include/py/homekit/mqtt/_payload.py diff --git a/py_include/homekit/mqtt/_util.py b/include/py/homekit/mqtt/_util.py similarity index 100% rename from py_include/homekit/mqtt/_util.py rename to include/py/homekit/mqtt/_util.py diff --git a/py_include/homekit/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py similarity index 100% rename from py_include/homekit/mqtt/_wrapper.py rename to include/py/homekit/mqtt/_wrapper.py diff --git a/py_include/homekit/mqtt/module/diagnostics.py b/include/py/homekit/mqtt/module/diagnostics.py similarity index 100% rename from py_include/homekit/mqtt/module/diagnostics.py rename to include/py/homekit/mqtt/module/diagnostics.py diff --git a/py_include/homekit/mqtt/module/inverter.py b/include/py/homekit/mqtt/module/inverter.py similarity index 100% rename from py_include/homekit/mqtt/module/inverter.py rename to include/py/homekit/mqtt/module/inverter.py diff --git a/py_include/homekit/mqtt/module/ota.py b/include/py/homekit/mqtt/module/ota.py similarity index 100% rename from py_include/homekit/mqtt/module/ota.py rename to include/py/homekit/mqtt/module/ota.py diff --git a/py_include/homekit/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py similarity index 100% rename from py_include/homekit/mqtt/module/relay.py rename to include/py/homekit/mqtt/module/relay.py diff --git a/py_include/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py similarity index 100% rename from py_include/homekit/mqtt/module/temphum.py rename to include/py/homekit/mqtt/module/temphum.py diff --git a/py_include/homekit/pio/__init__.py b/include/py/homekit/pio/__init__.py similarity index 100% rename from py_include/homekit/pio/__init__.py rename to include/py/homekit/pio/__init__.py diff --git a/py_include/homekit/pio/exceptions.py b/include/py/homekit/pio/exceptions.py similarity index 100% rename from py_include/homekit/pio/exceptions.py rename to include/py/homekit/pio/exceptions.py diff --git a/py_include/homekit/pio/products.py b/include/py/homekit/pio/products.py similarity index 100% rename from py_include/homekit/pio/products.py rename to include/py/homekit/pio/products.py diff --git a/py_include/homekit/relay/__init__.py b/include/py/homekit/relay/__init__.py similarity index 100% rename from py_include/homekit/relay/__init__.py rename to include/py/homekit/relay/__init__.py diff --git a/py_include/homekit/relay/__init__.pyi b/include/py/homekit/relay/__init__.pyi similarity index 100% rename from py_include/homekit/relay/__init__.pyi rename to include/py/homekit/relay/__init__.pyi diff --git a/py_include/homekit/relay/sunxi_h3_client.py b/include/py/homekit/relay/sunxi_h3_client.py similarity index 100% rename from py_include/homekit/relay/sunxi_h3_client.py rename to include/py/homekit/relay/sunxi_h3_client.py diff --git a/py_include/homekit/relay/sunxi_h3_server.py b/include/py/homekit/relay/sunxi_h3_server.py similarity index 100% rename from py_include/homekit/relay/sunxi_h3_server.py rename to include/py/homekit/relay/sunxi_h3_server.py diff --git a/py_include/homekit/soundsensor/__init__.py b/include/py/homekit/soundsensor/__init__.py similarity index 100% rename from py_include/homekit/soundsensor/__init__.py rename to include/py/homekit/soundsensor/__init__.py diff --git a/py_include/homekit/soundsensor/__init__.pyi b/include/py/homekit/soundsensor/__init__.pyi similarity index 100% rename from py_include/homekit/soundsensor/__init__.pyi rename to include/py/homekit/soundsensor/__init__.pyi diff --git a/py_include/homekit/soundsensor/node.py b/include/py/homekit/soundsensor/node.py similarity index 100% rename from py_include/homekit/soundsensor/node.py rename to include/py/homekit/soundsensor/node.py diff --git a/py_include/homekit/soundsensor/server.py b/include/py/homekit/soundsensor/server.py similarity index 100% rename from py_include/homekit/soundsensor/server.py rename to include/py/homekit/soundsensor/server.py diff --git a/py_include/homekit/soundsensor/server_client.py b/include/py/homekit/soundsensor/server_client.py similarity index 100% rename from py_include/homekit/soundsensor/server_client.py rename to include/py/homekit/soundsensor/server_client.py diff --git a/py_include/homekit/telegram/__init__.py b/include/py/homekit/telegram/__init__.py similarity index 100% rename from py_include/homekit/telegram/__init__.py rename to include/py/homekit/telegram/__init__.py diff --git a/py_include/homekit/telegram/_botcontext.py b/include/py/homekit/telegram/_botcontext.py similarity index 100% rename from py_include/homekit/telegram/_botcontext.py rename to include/py/homekit/telegram/_botcontext.py diff --git a/py_include/homekit/telegram/_botdb.py b/include/py/homekit/telegram/_botdb.py similarity index 100% rename from py_include/homekit/telegram/_botdb.py rename to include/py/homekit/telegram/_botdb.py diff --git a/py_include/homekit/telegram/_botlang.py b/include/py/homekit/telegram/_botlang.py similarity index 100% rename from py_include/homekit/telegram/_botlang.py rename to include/py/homekit/telegram/_botlang.py diff --git a/py_include/homekit/telegram/_botutil.py b/include/py/homekit/telegram/_botutil.py similarity index 100% rename from py_include/homekit/telegram/_botutil.py rename to include/py/homekit/telegram/_botutil.py diff --git a/py_include/homekit/telegram/aio.py b/include/py/homekit/telegram/aio.py similarity index 100% rename from py_include/homekit/telegram/aio.py rename to include/py/homekit/telegram/aio.py diff --git a/py_include/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py similarity index 100% rename from py_include/homekit/telegram/bot.py rename to include/py/homekit/telegram/bot.py diff --git a/py_include/homekit/telegram/config.py b/include/py/homekit/telegram/config.py similarity index 100% rename from py_include/homekit/telegram/config.py rename to include/py/homekit/telegram/config.py diff --git a/py_include/homekit/telegram/telegram.py b/include/py/homekit/telegram/telegram.py similarity index 100% rename from py_include/homekit/telegram/telegram.py rename to include/py/homekit/telegram/telegram.py diff --git a/py_include/homekit/temphum/__init__.py b/include/py/homekit/temphum/__init__.py similarity index 100% rename from py_include/homekit/temphum/__init__.py rename to include/py/homekit/temphum/__init__.py diff --git a/py_include/homekit/temphum/base.py b/include/py/homekit/temphum/base.py similarity index 100% rename from py_include/homekit/temphum/base.py rename to include/py/homekit/temphum/base.py diff --git a/py_include/homekit/temphum/i2c.py b/include/py/homekit/temphum/i2c.py similarity index 100% rename from py_include/homekit/temphum/i2c.py rename to include/py/homekit/temphum/i2c.py diff --git a/py_include/homekit/util.py b/include/py/homekit/util.py similarity index 100% rename from py_include/homekit/util.py rename to include/py/homekit/util.py diff --git a/py_include/pyA20/__init__.pyi b/include/py/pyA20/__init__.pyi similarity index 100% rename from py_include/pyA20/__init__.pyi rename to include/py/pyA20/__init__.pyi diff --git a/py_include/pyA20/gpio/connector.pyi b/include/py/pyA20/gpio/connector.pyi similarity index 100% rename from py_include/pyA20/gpio/connector.pyi rename to include/py/pyA20/gpio/connector.pyi diff --git a/py_include/pyA20/gpio/gpio.pyi b/include/py/pyA20/gpio/gpio.pyi similarity index 100% rename from py_include/pyA20/gpio/gpio.pyi rename to include/py/pyA20/gpio/gpio.pyi diff --git a/py_include/pyA20/gpio/port.pyi b/include/py/pyA20/gpio/port.pyi similarity index 100% rename from py_include/pyA20/gpio/port.pyi rename to include/py/pyA20/gpio/port.pyi diff --git a/py_include/pyA20/port.pyi b/include/py/pyA20/port.pyi similarity index 100% rename from py_include/pyA20/port.pyi rename to include/py/pyA20/port.pyi diff --git a/py_include/syncleo/__init__.py b/include/py/syncleo/__init__.py similarity index 100% rename from py_include/syncleo/__init__.py rename to include/py/syncleo/__init__.py diff --git a/py_include/syncleo/kettle.py b/include/py/syncleo/kettle.py similarity index 100% rename from py_include/syncleo/kettle.py rename to include/py/syncleo/kettle.py diff --git a/py_include/syncleo/protocol.py b/include/py/syncleo/protocol.py similarity index 100% rename from py_include/syncleo/protocol.py rename to include/py/syncleo/protocol.py diff --git a/platformio/dumb_mqtt/src/main.cpp b/pio/dumb_mqtt/src/main.cpp similarity index 100% rename from platformio/dumb_mqtt/src/main.cpp rename to pio/dumb_mqtt/src/main.cpp diff --git a/platformio/relayctl/src/main.cpp b/pio/relayctl/src/main.cpp similarity index 100% rename from platformio/relayctl/src/main.cpp rename to pio/relayctl/src/main.cpp diff --git a/platformio/temphum/src/main.cpp b/pio/temphum/src/main.cpp similarity index 100% rename from platformio/temphum/src/main.cpp rename to pio/temphum/src/main.cpp diff --git a/platformio/temphum_relayctl/src/main.cpp b/pio/temphum_relayctl/src/main.cpp similarity index 100% rename from platformio/temphum_relayctl/src/main.cpp rename to pio/temphum_relayctl/src/main.cpp 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/test/__py_include.py b/test/__py_include.py new file mode 100644 index 0000000..8f98830 --- /dev/null +++ b/test/__py_include.py @@ -0,0 +1,9 @@ +import sys +import os.path + +for _name in ('include/py',): + sys.path.extend([ + os.path.realpath( + os.path.join(os.path.dirname(os.path.join(__file__)), '..', _name) + ) + ]) \ No newline at end of file diff --git a/test/mqtt_relay_server_util.py b/test/mqtt_relay_server_util.py index ac6a9ae..6c02d75 100755 --- a/test/mqtt_relay_server_util.py +++ b/test/mqtt_relay_server_util.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 -from src.home.config import config -from src.home.mqtt.relay import MQTTRelayClient +from homekit.config import config if __name__ == '__main__': - config.load_app('test_mqtt_relay_server') - relay = MQTTRelayClient('test') - relay.connect_and_loop() + print(config) + # config.load_app('test_mqtt_relay_server') + # relay = MQTTRelayClient('test') + # relay.connect_and_loop() diff --git a/test/mqtt_relay_util.py b/test/mqtt_relay_util.py index 0d8c764..394bbe8 100755 --- a/test/mqtt_relay_util.py +++ b/test/mqtt_relay_util.py @@ -1,15 +1,9 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from argparse import ArgumentParser -from src.home.config import config -from src.home.mqtt.relay import MQTTRelayController +from homekit.config import config +from homekit.mqtt.relay import MQTTRelayController if __name__ == '__main__': diff --git a/test/test.py b/test/test.py index 413c25c..267a19f 100755 --- a/test/test.py +++ b/test/test.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import __py_include from homekit.relay import RelayClient diff --git a/test/test_amixer.py b/test/test_amixer.py index 464941e..e4abc73 100755 --- a/test/test_amixer.py +++ b/test/test_amixer.py @@ -1,12 +1,9 @@ #!/usr/bin/env python3 -import sys, os.path -sys.path.extend([ - os.path.realpath(os.path.join(os.path.dirname(os.path.join(__file__)), '..')), -]) +import __py_include from argparse import ArgumentParser -from src.home.config import config -from src.home.audio import amixer +from homekit.config import config +from homekit.audio import amixer def validate_control(input: str): diff --git a/test/test_api.py b/test/test_api.py index ecf8764..80ab62a 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,15 +1,9 @@ #!/usr/bin/env python3 -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include -from 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.api.types import BotType +from homekit.config import config if __name__ == '__main__': diff --git a/test/test_esp32_cam.py b/test/test_esp32_cam.py index 6a4ad25..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 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() diff --git a/test/test_inverter_monitor.py b/test/test_inverter_monitor.py index 621c0e9..3231bab 100755 --- a/test/test_inverter_monitor.py +++ b/test/test_inverter_monitor.py @@ -1,22 +1,11 @@ #!/usr/bin/env python3 -import cmd -import time -import logging -import socket -import sys -import threading -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import __py_include from enum import Enum, auto from typing import Optional -from src.home.util import stringify -from src.home.config import config -from src.home.inverter import ( +from homekit.util import stringify +from homekit.config import config +from homekit.inverter import ( wrapper_instance as inverter, InverterMonitor, diff --git a/test/test_ipcam_server_cleanup.py b/test/test_ipcam_server_cleanup.py index 5f313a4..ae8d31c 100644 --- a/test/test_ipcam_server_cleanup.py +++ b/test/test_ipcam_server_cleanup.py @@ -1,19 +1,13 @@ #!/usr/bin/env python3 -import shutil -import sys -import os -import re +import __py_include import logging -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import os +import shutil +import re from functools import cmp_to_key from datetime import datetime -from pprint import pprint -from src.home.config import config +from homekit.config import config logger = logging.getLogger(__name__) diff --git a/test/test_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 c0daceb..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 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__) diff --git a/test/test_send_fake_sound_hit.py b/test/test_send_fake_sound_hit.py index 61886cd..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, Addr +from homekit.util import send_datagram, stringify, Addr if __name__ == '__main__': diff --git a/test/test_sensors_plot.py b/test/test_sensors_plot.py deleted file mode 100755 index e69de29..0000000 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 77fe1ba..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 diff --git a/test/test_stopwatch.py b/test/test_stopwatch.py index 9dd7762..1da0fe7 100755 --- a/test/test_stopwatch.py +++ b/test/test_stopwatch.py @@ -1,3 +1,4 @@ +import __py_include from homekit.util import Stopwatch, StopwatchError from time import sleep diff --git a/test/test_telegram_aio_send_photo.py b/test/test_telegram_aio_send_photo.py index 4d05c03..019fa92 100644 --- a/test/test_telegram_aio_send_photo.py +++ b/test/test_telegram_aio_send_photo.py @@ -1,16 +1,9 @@ #!/usr/bin/env python3 +import __py_include import asyncio -import sys -import os.path -sys.path.extend([ - os.path.realpath( - os.path.join(os.path.dirname(os.path.join(__file__)), '..') - ) -]) +import homekit.telegram.aio as telegram -import src.home.telegram.aio as telegram - -from src.home.config import config +from homekit.config import config async def main(): From eaf8ccfd7de589ea540f810f626890d8cf267e04 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 23:23:00 +0300 Subject: [PATCH 08/51] readme: remove obsolete note --- README.md | 6 ------ 1 file changed, 6 deletions(-) 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 From 6055011d82fe001a8cb88359b322c8a8581cc987 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 23:25:31 +0300 Subject: [PATCH 09/51] arduino/esp-32: move files --- .gitignore | 2 +- .../CameraWebServer.ino | 0 .../CameraWebServer => ESP32CameraWebServer}/app_httpd.cpp | 0 .../CameraWebServer => ESP32CameraWebServer}/camera_index.h | 0 .../CameraWebServer => ESP32CameraWebServer}/camera_pins.h | 0 .../CameraWebServer => ESP32CameraWebServer}/index_ov2640.html | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename arduino/{esp32-cam/CameraWebServer => ESP32CameraWebServer}/CameraWebServer.ino (100%) rename arduino/{esp32-cam/CameraWebServer => ESP32CameraWebServer}/app_httpd.cpp (100%) rename arduino/{esp32-cam/CameraWebServer => ESP32CameraWebServer}/camera_index.h (100%) rename arduino/{esp32-cam/CameraWebServer => ESP32CameraWebServer}/camera_pins.h (100%) rename arduino/{esp32-cam/CameraWebServer => ESP32CameraWebServer}/index_ov2640.html (100%) diff --git a/.gitignore b/.gitignore index 6de5e71..9a32ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ __pycache__ /cpp /include/test.py /bin/test.py -/arduino/esp32-cam/CameraWebServer/wifi_password.h +/arduino/ESP32CameraWebServer/wifi_password.h cmake-build-* .pio platformio.ini diff --git a/arduino/esp32-cam/CameraWebServer/CameraWebServer.ino b/arduino/ESP32CameraWebServer/CameraWebServer.ino similarity index 100% rename from arduino/esp32-cam/CameraWebServer/CameraWebServer.ino rename to arduino/ESP32CameraWebServer/CameraWebServer.ino diff --git a/arduino/esp32-cam/CameraWebServer/app_httpd.cpp b/arduino/ESP32CameraWebServer/app_httpd.cpp similarity index 100% rename from arduino/esp32-cam/CameraWebServer/app_httpd.cpp rename to arduino/ESP32CameraWebServer/app_httpd.cpp diff --git a/arduino/esp32-cam/CameraWebServer/camera_index.h b/arduino/ESP32CameraWebServer/camera_index.h similarity index 100% rename from arduino/esp32-cam/CameraWebServer/camera_index.h rename to arduino/ESP32CameraWebServer/camera_index.h diff --git a/arduino/esp32-cam/CameraWebServer/camera_pins.h b/arduino/ESP32CameraWebServer/camera_pins.h similarity index 100% rename from arduino/esp32-cam/CameraWebServer/camera_pins.h rename to arduino/ESP32CameraWebServer/camera_pins.h diff --git a/arduino/esp32-cam/CameraWebServer/index_ov2640.html b/arduino/ESP32CameraWebServer/index_ov2640.html similarity index 100% rename from arduino/esp32-cam/CameraWebServer/index_ov2640.html rename to arduino/ESP32CameraWebServer/index_ov2640.html From ee0341e137f6a8dcf90d5a744e334f66b9d6d60a Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 01:34:08 +0300 Subject: [PATCH 10/51] fix platformio.ini generation --- bin/pio_ini.py | 26 +++++++++++++++++--------- include/py/homekit/config/__init__.py | 3 ++- include/py/homekit/pio/products.py | 4 ++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bin/pio_ini.py b/bin/pio_ini.py index 34ad395..2926234 100755 --- a/bin/pio_ini.py +++ b/bin/pio_ini.py @@ -8,17 +8,19 @@ from pprint import pprint from argparse import ArgumentParser, ArgumentError 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) @@ -83,7 +85,8 @@ def bsd_get(product_config: dict, defines[f'CONFIG_{define_name}'] = f'HOMEKIT_{attr_value.upper()}' return if kwargs['type'] == 'bool': - defines[f'CONFIG_{define_name}'] = True + if attr_value is True: + defines[f'CONFIG_{define_name}'] = True return defines[f'CONFIG_{define_name}'] = str(attr_value) bsd_walk(product_config, f) @@ -124,6 +127,11 @@ if __name__ == '__main__': raise ArgumentError(None, f'target {arg.target} not found for product {product}') bsd, bsd_enums = bsd_get(product_config, arg) + print('>>> bsd:') + pprint(bsd) + print('>>> bsd_enums:') + pprint(bsd_enums) + ini = platformio_ini(product_config=product_config, target=arg.target, build_specific_defines=bsd, diff --git a/include/py/homekit/config/__init__.py b/include/py/homekit/config/__init__.py index 2fa5214..8fedfa6 100644 --- a/include/py/homekit/config/__init__.py +++ b/include/py/homekit/config/__init__.py @@ -5,7 +5,8 @@ from .config import ( Translation, config, is_development_mode, - setup_logging + setup_logging, + CONFIG_DIRECTORIES ) from ._configs import ( LinuxBoardsConfig, diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py index 388da03..c4fcd73 100644 --- a/include/py/homekit/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -8,8 +8,8 @@ from collections import OrderedDict _logger = logging.getLogger(__name__) _products_dir = os.path.join( os.path.dirname(__file__), - '..', '..', '..', - 'platformio' + '..', '..', '..', '..', + 'pio' ) From 1215bbf102498fb585b310ba0b5043df875f71fd Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 01:34:50 +0300 Subject: [PATCH 11/51] pio_ini: remove debug code that breaks it :( --- bin/pio_ini.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bin/pio_ini.py b/bin/pio_ini.py index 2926234..7254eca 100755 --- a/bin/pio_ini.py +++ b/bin/pio_ini.py @@ -4,7 +4,6 @@ import yaml import re import __py_include -from pprint import pprint from argparse import ArgumentParser, ArgumentError from homekit.pio import get_products, platformio_ini from homekit.pio.exceptions import ProductConfigNotFoundError @@ -127,10 +126,6 @@ if __name__ == '__main__': raise ArgumentError(None, f'target {arg.target} not found for product {product}') bsd, bsd_enums = bsd_get(product_config, arg) - print('>>> bsd:') - pprint(bsd) - print('>>> bsd_enums:') - pprint(bsd_enums) ini = platformio_ini(product_config=product_config, target=arg.target, From 00b3cd120f6357a35ef7e8b1c3ffad458a068266 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 01:43:36 +0300 Subject: [PATCH 12/51] pio: fix libs paths --- include/pio/libs/main/library.json | 6 +++--- include/pio/libs/mqtt_module_diagnostics/library.json | 4 ++-- include/pio/libs/mqtt_module_ota/library.json | 6 +++--- include/pio/libs/mqtt_module_relay/library.json | 6 +++--- include/pio/libs/mqtt_module_temphum/library.json | 4 ++-- include/py/homekit/pio/products.py | 6 ++++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/include/pio/libs/main/library.json b/include/pio/libs/main/library.json index 728d4f8..c5586d8 100644 --- a/include/pio/libs/main/library.json +++ b/include/pio/libs/main/library.json @@ -1,12 +1,12 @@ { "name": "homekit_main", - "version": "1.0.10", + "version": "1.0.11", "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" + "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/include/pio/libs/mqtt_module_diagnostics/library.json b/include/pio/libs/mqtt_module_diagnostics/library.json index a3d3244..70acb79 100644 --- a/include/pio/libs/mqtt_module_diagnostics/library.json +++ b/include/pio/libs/mqtt_module_diagnostics/library.json @@ -1,10 +1,10 @@ { "name": "homekit_mqtt_module_diagnostics", - "version": "1.0.2", + "version": "1.0.3", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt" + "homekit_mqtt": "file://../../include/pio/libs/mqtt" } } diff --git a/include/pio/libs/mqtt_module_ota/library.json b/include/pio/libs/mqtt_module_ota/library.json index 4f40a47..1577fed 100644 --- a/include/pio/libs/mqtt_module_ota/library.json +++ b/include/pio/libs/mqtt_module_ota/library.json @@ -1,11 +1,11 @@ { "name": "homekit_mqtt_module_ota", - "version": "1.0.5", + "version": "1.0.6", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_led": "file://../common/libs/led", - "homekit_mqtt": "file://../common/libs/mqtt" + "homekit_led": "file://../../include/pio/libs/led", + "homekit_mqtt": "file://../../include/pio/libs/mqtt" } } diff --git a/include/pio/libs/mqtt_module_relay/library.json b/include/pio/libs/mqtt_module_relay/library.json index 6cbbfb0..18a510c 100644 --- a/include/pio/libs/mqtt_module_relay/library.json +++ b/include/pio/libs/mqtt_module_relay/library.json @@ -1,11 +1,11 @@ { "name": "homekit_mqtt_module_relay", - "version": "1.0.5", + "version": "1.0.6", "build": { "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt", - "homekit_relay": "file://../common/libs/relay" + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_relay": "file://../../include/pio/libs/relay" } } diff --git a/include/pio/libs/mqtt_module_temphum/library.json b/include/pio/libs/mqtt_module_temphum/library.json index 068debd..c7ee7af 100644 --- a/include/pio/libs/mqtt_module_temphum/library.json +++ b/include/pio/libs/mqtt_module_temphum/library.json @@ -5,7 +5,7 @@ "flags": "-I../../include" }, "dependencies": { - "homekit_mqtt": "file://../common/libs/mqtt", - "homekit_temphum": "file://../common/libs/temphum" + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_temphum": "file://../../include/pio/libs/temphum" } } diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py index c4fcd73..a0e7a1f 100644 --- a/include/py/homekit/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -89,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: @@ -107,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() From 1d0b9c5d1c90c4f7c7a6eb0c3cf32ffb843f2533 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 02:07:51 +0300 Subject: [PATCH 13/51] telegram bots: get rid of requests logging via webapi --- bin/inverter_bot.py | 2 -- bin/polaris_kettle_bot.py | 4 ---- bin/pump_bot.py | 2 -- bin/sensors_bot.py | 4 ---- bin/sound_bot.py | 4 +--- bin/web_api.py | 26 +-------------------- include/py/homekit/api/types/__init__.py | 1 - include/py/homekit/api/types/types.py | 11 --------- include/py/homekit/api/web_api_client.py | 10 -------- include/py/homekit/database/bots.py | 10 -------- include/py/homekit/telegram/_botutil.py | 17 -------------- include/py/homekit/telegram/bot.py | 29 ++++++++---------------- test/test_api.py | 5 ++-- 13 files changed, 13 insertions(+), 112 deletions(-) diff --git a/bin/inverter_bot.py b/bin/inverter_bot.py index fdfe436..7da21aa 100755 --- a/bin/inverter_bot.py +++ b/bin/inverter_bot.py @@ -28,7 +28,6 @@ from homekit.inverter.types import ( OutputSourcePriority ) from homekit.database.inverter_time_formats import FormatDate -from homekit.api.types import BotType from homekit.api import WebApiClient from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton @@ -921,7 +920,6 @@ class InverterStore(bot.BotDatabase): inverter.init(host=config['inverter']['ip'], port=config['inverter']['port']) bot.set_database(InverterStore()) -bot.enable_logging(BotType.INVERTER) bot.add_conversation(SettingsConversation(enable_back=True)) bot.add_conversation(ConsumptionConversation(enable_back=True)) diff --git a/bin/polaris_kettle_bot.py b/bin/polaris_kettle_bot.py index 3a24fe0..05c2aae 100755 --- a/bin/polaris_kettle_bot.py +++ b/bin/polaris_kettle_bot.py @@ -10,7 +10,6 @@ import threading import paho.mqtt.client as mqtt from homekit.telegram import bot -from homekit.api.types import BotType from homekit.mqtt import Mqtt from homekit.config import config from homekit.util import chunks @@ -738,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/bin/pump_bot.py b/bin/pump_bot.py index 08d0dc6..2583c5f 100755 --- a/bin/pump_bot.py +++ b/bin/pump_bot.py @@ -11,7 +11,6 @@ from homekit.config import config, is_development_mode from homekit.telegram import bot from homekit.telegram._botutil import user_any_name from homekit.relay.sunxi_h3_client import RelayClient -from homekit.api.types import BotType from homekit.mqtt import MqttNode, MqttWrapper, MqttPayload from homekit.mqtt.module.relay import MqttPowerStatusPayload, MqttRelayModule from homekit.mqtt.module.temphum import MqttTemphumDataPayload @@ -248,7 +247,6 @@ if __name__ == '__main__': mqtt.connect_and_loop(loop_forever=False) - bot.enable_logging(BotType.PUMP) bot.run() try: diff --git a/bin/sensors_bot.py b/bin/sensors_bot.py index c2b0070..43932e1 100755 --- a/bin/sensors_bot.py +++ b/bin/sensors_bot.py @@ -20,7 +20,6 @@ from homekit.telegram import bot from homekit.util import chunks, MySimpleSocketClient from homekit.api import WebApiClient from homekit.api.types import ( - BotType, TemperatureSensorLocation ) @@ -176,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/bin/sound_bot.py b/bin/sound_bot.py index 518151d..fa22ba7 100755 --- a/bin/sound_bot.py +++ b/bin/sound_bot.py @@ -11,7 +11,7 @@ from typing import Optional, List, Dict, Tuple from homekit.config import config from homekit.api import WebApiClient -from homekit.api.types import SoundSensorLocation, BotType +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 @@ -884,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/bin/web_api.py b/bin/web_api.py index 0e0fd0b..e543d22 100755 --- a/bin/web_api.py +++ b/bin/web_api.py @@ -11,7 +11,7 @@ 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 BotType, TemperatureSensorLocation, SoundSensorLocation +from homekit.api.types import TemperatureSensorLocation, SoundSensorLocation from homekit.media import SoundRecordStorage @@ -126,30 +126,6 @@ class WebAPIServer(http.HTTPServer): BotsDatabase().add_sound_hits(hits, datetime.now()) return self.ok() - async def POST_bot_request_log(self, req: http.Request): - data = await req.post() - - try: - user_id = int(data['user_id']) - except KeyError: - user_id = 0 - - try: - message = data['message'] - except KeyError: - message = '' - - bot = BotType(int(data['bot'])) - - # validate message - if message.strip() == '': - raise ValueError('message can\'t be empty') - - # add record to the database - BotsDatabase().add_request(bot, user_id, message) - - return self.ok() - async def POST_openwrt_log(self, req: http.Request): data = await req.post() diff --git a/include/py/homekit/api/types/__init__.py b/include/py/homekit/api/types/__init__.py index 9f27ff6..22ce4e6 100644 --- a/include/py/homekit/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/include/py/homekit/api/types/types.py b/include/py/homekit/api/types/types.py index 981e798..294a712 100644 --- a/include/py/homekit/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/include/py/homekit/api/web_api_client.py b/include/py/homekit/api/web_api_client.py index 15c1915..f9a8963 100644 --- a/include/py/homekit/api/web_api_client.py +++ b/include/py/homekit/api/web_api_client.py @@ -57,16 +57,6 @@ class WebApiClient: # 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): diff --git a/include/py/homekit/database/bots.py b/include/py/homekit/database/bots.py index cde48b9..fb5f326 100644 --- a/include/py/homekit/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/include/py/homekit/telegram/_botutil.py b/include/py/homekit/telegram/_botutil.py index 111a704..4fbbf28 100644 --- a/include/py/homekit/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 homekit.api import WebApiClient as APIClient -from homekit.api.types import BotType -from homekit.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/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index 2e33bea..5ed8b06 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -21,12 +21,10 @@ from telegram.ext.filters import BaseFilter from telegram.error import TimedOut from homekit.config import config -from homekit.api import WebApiClient -from homekit.api.types import BotType 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 @@ -39,7 +37,6 @@ _cancel_and_back_filter = filters.Text(lang.all('back') + lang.all('cancel')) _logger = logging.getLogger(__name__) _application: Optional[Application] = None -_reporting: Optional[ReportingHelper] = None _exception_handler: Optional[Coroutine] = None _dispatcher = None _markup_getter: Optional[callable] = None @@ -511,22 +508,14 @@ async def _default_any_handler(ctx: Context): await ctx.reply(ctx.lang('invalid_command')) -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 enable_logging(bot_type: BotType): - api = WebApiClient(timeout=3) - api.enable_async() - - global _reporting - _reporting = ReportingHelper(api, bot_type) +# 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 notify_all(text_getter: callable, diff --git a/test/test_api.py b/test/test_api.py index 80ab62a..b35a597 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -2,12 +2,11 @@ import __py_include from homekit.api import WebApiClient -from homekit.api.types import BotType from homekit.config import config if __name__ == '__main__': config.load_app('test_api') - api = WebApiClient() - print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) + # api = WebApiClient() + # print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) From eaab12b8f4722ceae1039e4745088c555d6cbd1e Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 02:27:43 +0300 Subject: [PATCH 14/51] pump_bot: port to new config scheme and PTB 20 --- bin/pump_bot.py | 152 ++++++++++++++++++----------- bin/relay_mqtt_bot.py | 3 +- include/py/homekit/telegram/bot.py | 28 +++--- 3 files changed, 113 insertions(+), 70 deletions(-) diff --git a/bin/pump_bot.py b/bin/pump_bot.py index 2583c5f..e00e844 100755 --- a/bin/pump_bot.py +++ b/bin/pump_bot.py @@ -1,27 +1,62 @@ #!/usr/bin/env python3 import __py_include +import sys +import asyncio from enum import Enum -from typing import Optional +from typing import Optional, Union from telegram import ReplyKeyboardMarkup, User from time import time from datetime import datetime -from homekit.config import config, is_development_mode +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 +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 -config.load_app('pump_bot') +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] -mqtt: Optional[MqttWrapper] = None -mqtt_node: Optional[MqttNode] = None -mqtt_relay_module: Optional[MqttRelayModule] = None time_format = '%d.%m.%Y, %H:%M:%S' watering_mcu_status = { @@ -99,81 +134,89 @@ class UserAction(Enum): def get_relay() -> RelayClient: - relay = RelayClient(host=config['relay']['ip'], port=config['relay']['port']) + relay = RelayClient(host=config.app_config['pump_relay_addr'].host, + port=config.app_config['pump_relay_addr'].port) relay.connect() return relay -def on(ctx: bot.Context, silent=False) -> None: +async def on(ctx: bot.Context, silent=False) -> None: get_relay().on() - ctx.reply(ctx.lang('done')) + futures = [ctx.reply(ctx.lang('done'))] if not silent: - notify(ctx.user, UserAction.ON) + futures.append(notify(ctx.user, UserAction.ON)) + await asyncio.gather(*futures) -def off(ctx: bot.Context, silent=False) -> None: +async def off(ctx: bot.Context, silent=False) -> None: get_relay().off() - ctx.reply(ctx.lang('done')) + futures = [ctx.reply(ctx.lang('done'))] if not silent: - notify(ctx.user, UserAction.OFF) + futures.append(notify(ctx.user, UserAction.OFF)) + await asyncio.gather(*futures) -def watering_on(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(True, config.get('mqtt_water_relay.secret')) - ctx.reply(ctx.lang('sent')) - notify(ctx.user, UserAction.WATERING_ON) +async def watering_on(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(True) + await asyncio.gather( + ctx.reply(ctx.lang('sent')), + notify(ctx.user, UserAction.WATERING_ON) + ) -def watering_off(ctx: bot.Context) -> None: - mqtt_relay_module.switchpower(False, config.get('mqtt_water_relay.secret')) - ctx.reply(ctx.lang('sent')) - notify(ctx.user, UserAction.WATERING_OFF) +async def watering_off(ctx: bot.Context) -> None: + mqtt_relay_module.switchpower(False) + await asyncio.gather( + ctx.reply(ctx.lang('sent')), + notify(ctx.user, UserAction.WATERING_OFF) + ) -def notify(user: User, action: UserAction) -> None: +async def notify(user: User, action: UserAction) -> None: notification_key = 'user_watering_notification' if action in (UserAction.WATERING_ON, UserAction.WATERING_OFF) else 'user_action_notification' + def text_getter(lang: str): action_name = bot.lang.get(f'user_action_{action.value}', lang) user_name = user_any_name(user) return 'ℹ ' + bot.lang.get(notification_key, lang, user.id, user_name, action_name) - bot.notify_all(text_getter, exclude=(user.id,)) + await bot.notify_all(text_getter, exclude=(user.id,)) @bot.handler(message='enable') -def enable_handler(ctx: bot.Context) -> None: - on(ctx) +async def enable_handler(ctx: bot.Context) -> None: + await on(ctx) @bot.handler(message='enable_silently') -def enable_s_handler(ctx: bot.Context) -> None: - on(ctx, True) +async def enable_s_handler(ctx: bot.Context) -> None: + await on(ctx, True) @bot.handler(message='disable') -def disable_handler(ctx: bot.Context) -> None: - off(ctx) +async def disable_handler(ctx: bot.Context) -> None: + await off(ctx) @bot.handler(message='start_watering') -def start_watering(ctx: bot.Context) -> None: - watering_on(ctx) +async def start_watering(ctx: bot.Context) -> None: + await watering_on(ctx) @bot.handler(message='stop_watering') -def stop_watering(ctx: bot.Context) -> None: - watering_off(ctx) +async def stop_watering(ctx: bot.Context) -> None: + await watering_off(ctx) @bot.handler(message='disable_silently') -def disable_s_handler(ctx: bot.Context) -> None: - off(ctx, True) +async def disable_s_handler(ctx: bot.Context) -> None: + await off(ctx, True) @bot.handler(message='status') -def status(ctx: bot.Context) -> None: - ctx.reply( +async def status(ctx: bot.Context) -> None: + await ctx.reply( ctx.lang('enabled') if get_relay().status() == 'on' else ctx.lang('disabled') ) @@ -186,7 +229,7 @@ def _get_timestamp_as_string(timestamp: int) -> str: @bot.handler(message='watering_status') -def watering_status(ctx: bot.Context) -> None: +async def watering_status(ctx: bot.Context) -> None: buf = '' if 0 < watering_mcu_status["last_time"] < time()-1800: buf += 'WARNING! long time no reports from mcu! maybe something\'s wrong\n' @@ -195,13 +238,13 @@ def watering_status(ctx: bot.Context) -> None: buf += f'boot time: {_get_timestamp_as_string(watering_mcu_status["last_boot_time"])}\n' buf += 'relay opened: ' + ('yes' if watering_mcu_status['relay_opened'] else 'no') + '\n' buf += f'ambient temp & humidity: {watering_mcu_status["ambient_temp"]} °C, {watering_mcu_status["ambient_rh"]}%' - ctx.reply(buf) + await ctx.reply(buf) @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: buttons = [] - if ctx.user_id in config['bot']['silent_users']: + if ctx.user_id in config.app_config.get_user_ids(PumpBotUserListType.SILENT): buttons.append([ctx.lang('enable_silently'), ctx.lang('disable_silently')]) buttons.append([ctx.lang('enable'), ctx.lang('disable'), ctx.lang('status')],) buttons.append([ctx.lang('start_watering'), ctx.lang('stop_watering'), ctx.lang('watering_status')]) @@ -234,22 +277,21 @@ def mqtt_payload_callback(mqtt_node: MqttNode, payload: MqttPayload): watering_mcu_status['relay_opened'] = payload.opened -if __name__ == '__main__': - mqtt = MqttWrapper() - mqtt_node = MqttNode(node_id=config.get('mqtt_water_relay.node_id')) - if is_development_mode(): - mqtt_node.load_module('diagnostics') +mqtt = MqttWrapper(client_id='pump_bot') +mqtt_node = MqttNode(node_id=config.app_config['watering_relay_node']) +if is_development_mode(): + mqtt_node.load_module('diagnostics') - mqtt_node.load_module('temphum') - mqtt_relay_module = mqtt_node.load_module('relay') +mqtt_node.load_module('temphum') +mqtt_relay_module = mqtt_node.load_module('relay') - mqtt_node.add_payload_callback(mqtt_payload_callback) +mqtt_node.add_payload_callback(mqtt_payload_callback) - mqtt.connect_and_loop(loop_forever=False) +mqtt.connect_and_loop(loop_forever=False) - bot.run() +bot.run() - try: - mqtt.disconnect() - except: - pass +try: + mqtt.disconnect() +except: + pass diff --git a/bin/relay_mqtt_bot.py b/bin/relay_mqtt_bot.py index 1c1cc94..3ad0a9b 100755 --- a/bin/relay_mqtt_bot.py +++ b/bin/relay_mqtt_bot.py @@ -10,8 +10,7 @@ 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 -from homekit.mqtt import MqttNodesConfig +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 diff --git a/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index 5ed8b06..cf68b1d 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -26,6 +26,7 @@ from ._botlang import lang, languages from ._botdb import BotDatabase from ._botutil import exc2text, IgnoreMarkup from ._botcontext import Context +from .config import TelegramUserListType db: Optional[BotDatabase] = None @@ -518,29 +519,30 @@ async def _default_any_handler(ctx: Context): # _reporting.report(update.callback_query.message, text=update.callback_query.data) -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)) - _application.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) - _application.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): From a3d6fadb2e99a87346d5b4f2c97755cc6f17f3b7 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 02:33:22 +0300 Subject: [PATCH 15/51] gpiorelayd: get rid of config, use command line arguments instead --- bin/gpiorelayd.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bin/gpiorelayd.py b/bin/gpiorelayd.py index 1f4d2e2..89ba78e 100755 --- a/bin/gpiorelayd.py +++ b/bin/gpiorelayd.py @@ -4,6 +4,8 @@ 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 @@ -11,14 +13,19 @@ logger = logging.getLogger(__name__) if __name__ == '__main__': - if not os.getegid() == 0: + if os.getegid() != 0: sys.exit('Must be run as root.') - config.load_app() + 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: - s = RelayServer(pinname=config.get('relayd.pin'), - addr=config.get_addr('relayd.listen')) - s.run() + RelayServer(pinname=arg.pin, addr=listen).run() except KeyboardInterrupt: logger.info('Exiting...') From d1331c2904703efc2e10b6942170726930b630c8 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 02:35:52 +0300 Subject: [PATCH 16/51] gpiorelayd: update systemd service unit file --- systemd/gpiorelayd@.service | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/systemd/gpiorelayd@.service b/systemd/gpiorelayd@.service index a3a8356..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/bin/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] From 975d2bc6ed6d588187fea4bb538e04ac30cbd989 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 03:06:54 +0300 Subject: [PATCH 17/51] telegram/bot: fix missing async/await in some functions --- include/py/homekit/telegram/bot.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index cf68b1d..8a78c6f 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -545,27 +545,27 @@ async def notify_user(user_id: int, text: Union[str, Exception], **kwargs) -> No **kwargs) -def send_photo(user_id, **kwargs): - _application.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): - _application.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): - _application.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): - _application.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): - _application.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): From 0109d6c01db94822757cd7cb84034dd6f4d6cea8 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 03:20:01 +0300 Subject: [PATCH 18/51] inverter bot: migrate to PTB 20 (not tested yet) --- bin/inverter_bot.py | 239 +++++++++++++------------ include/py/homekit/inverter/monitor.py | 2 +- include/py/homekit/telegram/bot.py | 20 +-- 3 files changed, 140 insertions(+), 121 deletions(-) diff --git a/bin/inverter_bot.py b/bin/inverter_bot.py index 7da21aa..032f513 100755 --- a/bin/inverter_bot.py +++ b/bin/inverter_bot.py @@ -5,6 +5,7 @@ import datetime import json import itertools import sys +import asyncio import __py_include from inverterd import Format, InverterError @@ -347,8 +348,11 @@ def monitor_charging(event: ChargingEvent, **kwargs) -> None: key = f'chrg_evt_{key}' if is_util: key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang, *args) + + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang, *args) + ) ) @@ -363,9 +367,11 @@ def monitor_battery(state: BatteryState, v: float, load_watts: int) -> None: logger.error('unknown battery state:', state) return - bot.notify_all( - lambda lang: bot.lang.get('battery_level_changed', lang, - emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('battery_level_changed', lang, + emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + ) ) @@ -375,14 +381,18 @@ def monitor_util(event: ACPresentEvent): else: key = 'disconnected' key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang) + ) ) def monitor_error(error: str) -> None: - bot.notify_all( - lambda lang: bot.lang.get('error_message', lang, error) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('error_message', lang, error) + ) ) @@ -392,35 +402,37 @@ def osp_change_cb(new_osp: OutputSourcePriority, setosp(new_osp) - bot.notify_all( - lambda lang: bot.lang.get('osp_auto_changed_notification', lang, - bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('osp_auto_changed_notification', lang, + bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + ) ) @bot.handler(command='status') -def full_status(ctx: bot.Context) -> None: +async def full_status(ctx: bot.Context) -> None: status = inverter.exec('get-status', format=Format.TABLE) - ctx.reply(beautify_table(status)) + await ctx.reply(beautify_table(status)) @bot.handler(command='config') -def full_rated(ctx: bot.Context) -> None: +async def full_rated(ctx: bot.Context) -> None: rated = inverter.exec('get-rated', format=Format.TABLE) - ctx.reply(beautify_table(rated)) + await ctx.reply(beautify_table(rated)) @bot.handler(command='errors') -def full_errors(ctx: bot.Context) -> None: +async def full_errors(ctx: bot.Context) -> None: errors = inverter.exec('get-errors', format=Format.TABLE) - ctx.reply(beautify_table(errors)) + await ctx.reply(beautify_table(errors)) @bot.handler(command='flags') -def flags_handler(ctx: bot.Context) -> None: +async def flags_handler(ctx: bot.Context) -> None: flags = inverter.exec('get-flags')['data'] text, markup = build_flags_keyboard(flags, ctx) - ctx.reply(text, markup=markup) + await ctx.reply(text, markup=markup) def build_flags_keyboard(flags: dict, ctx: bot.Context) -> Tuple[str, InlineKeyboardMarkup]: @@ -477,11 +489,11 @@ class SettingsConversation(bot.conversation): REDISCHARGE_VOLTAGES = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58] @bot.conventer(START, message='settings') - def start_enter(self, ctx: bot.Context): + async def start_enter(self, ctx: bot.Context): buttons = list(chunks(list(self.START_BUTTONS), 2)) buttons.reverse() - return self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, - with_cancel=True) + return await self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, + with_cancel=True) @bot.convinput(START, messages={ 'settings_osp': OSP, @@ -490,16 +502,16 @@ class SettingsConversation(bot.conversation): 'settings_bat_cut_off_voltage': BAT_CUT_OFF_VOLTAGE, 'settings_ac_max_charging_current': AC_MAX_CHARGING_CURRENT }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(OSP) - def osp_enter(self, ctx: bot.Context): - return self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, - with_back=True) + async def osp_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, + with_back=True) @bot.convinput(OSP, messages=OSP_BUTTONS) - def osp_input(self, ctx: bot.Context): + async def osp_input(self, ctx: bot.Context): selected_sp = None for sp in OutputSourcePriority: if ctx.text == ctx.lang(f'settings_osp_{sp.value.lower()}'): @@ -512,25 +524,28 @@ class SettingsConversation(bot.conversation): # apply the mode setosp(selected_sp) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()) + 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,) + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('osp_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(f'settings_osp_{selected_sp.value.lower()}', lang)), + exclude=(ctx.user_id,) + ) ) + return self.END @bot.conventer(AC_PRESET) - def acpreset_enter(self, ctx: bot.Context): - return self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, - with_back=True) + async def acpreset_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, + with_back=True) @bot.convinput(AC_PRESET, messages=AC_PRESET_BUTTONS) - def acpreset_input(self, ctx: bot.Context): + async def acpreset_input(self, ctx: bot.Context): if monitor.active_current is not None: raise RuntimeError('generator charging program is active') @@ -547,85 +562,88 @@ class SettingsConversation(bot.conversation): # save bot.db.set_param('ac_mode', str(newmode.value)) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()) + 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,) + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('ac_mode_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(str(newmode.value), lang)), + exclude=(ctx.user_id,) + ) ) + return self.END @bot.conventer(BAT_THRESHOLDS_1) - def thresholds1_enter(self, ctx: bot.Context): + async def thresholds1_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.RECHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_1, messages=list(map(lambda n: f'{n} V', RECHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds1_input(self, ctx: bot.Context): + async def thresholds1_input(self, ctx: bot.Context): v = self._parse_voltage(ctx.text) ctx.user_data['bat_thrsh_v1'] = v - return self.invoke(self.BAT_THRESHOLDS_2, ctx) + return await self.invoke(self.BAT_THRESHOLDS_2, ctx) @bot.conventer(BAT_THRESHOLDS_2) - def thresholds2_enter(self, ctx: bot.Context): + async def thresholds2_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.REDISCHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_2, messages=list(map(lambda n: f'{n} V', REDISCHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds2_input(self, ctx: bot.Context): + async def thresholds2_input(self, ctx: bot.Context): v2 = v = self._parse_voltage(ctx.text) v1 = ctx.user_data['bat_thrsh_v1'] del ctx.user_data['bat_thrsh_v1'] response = inverter.exec('set-charge-thresholds', (v1, v2)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(AC_MAX_CHARGING_CURRENT) - def ac_max_enter(self, ctx: bot.Context): + async def ac_max_enter(self, ctx: bot.Context): buttons = self._get_allowed_ac_charge_amps() buttons = map(lambda n: f'{n} A', buttons) buttons = [list(buttons)] - return self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(AC_MAX_CHARGING_CURRENT, regex=r'^\d+ A$') - def ac_max_input(self, ctx: bot.Context): + async def ac_max_input(self, ctx: bot.Context): a = self._parse_amps(ctx.text) allowed = self._get_allowed_ac_charge_amps() if a not in allowed: raise ValueError('input is not allowed') response = inverter.exec('set-max-ac-charge-current', (0, a)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(BAT_CUT_OFF_VOLTAGE) - def cutoff_enter(self, ctx: bot.Context): - return self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, - with_back=True) + async def cutoff_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, + with_back=True) @bot.convinput(BAT_CUT_OFF_VOLTAGE, regex=r'^(\d{2}(\.\d{1})?)$') - def cutoff_input(self, ctx: bot.Context): + async def cutoff_input(self, ctx: bot.Context): v = float(ctx.text) if 40.0 <= v <= 48.0: response = inverter.exec('set-battery-cutoff-voltage', (v,)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) else: raise ValueError('invalid voltage') @@ -660,38 +678,38 @@ class ConsumptionConversation(bot.conversation): INTERVAL_BUTTONS_FLAT = list(itertools.chain.from_iterable(INTERVAL_BUTTONS)) @bot.conventer(START, message='consumption') - def start_enter(self, ctx: bot.Context): - return self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], - with_cancel=True) + async def start_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], + with_cancel=True) @bot.convinput(START, messages={ 'consumption_total': TOTAL, 'consumption_grid': GRID }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(TOTAL) - def total_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.TOTAL) + async def total_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.TOTAL) @bot.conventer(GRID) - def grid_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.GRID) + async def grid_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.GRID) - def _render_interval_btns(self, ctx: bot.Context, state): - return self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, - with_back=True) + async def _render_interval_btns(self, ctx: bot.Context, state): + return await self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, + with_back=True) @bot.convinput(TOTAL, messages=INTERVAL_BUTTONS_FLAT) - def total_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.TOTAL) + async def total_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.TOTAL) @bot.convinput(GRID, messages=INTERVAL_BUTTONS_FLAT) - def grid_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.GRID) + async def grid_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.GRID) - def _render_interval_results(self, ctx: bot.Context, state): + async def _render_interval_results(self, ctx: bot.Context, state): # if ctx.text == ctx.lang('to_select_interval'): # TODO # pass @@ -715,41 +733,43 @@ class ConsumptionConversation(bot.conversation): # [InlineKeyboardButton(ctx.lang('please_wait'), callback_data='wait')] # ]) - message = ctx.reply(ctx.lang('consumption_request_sent'), - markup=bot.IgnoreMarkup()) + message = await ctx.reply(ctx.lang('consumption_request_sent'), + markup=bot.IgnoreMarkup()) api = WebApiClient(timeout=60) method = 'inverter_get_consumed_energy' if state == self.TOTAL else 'inverter_get_grid_consumed_energy' try: wh = getattr(api, method)(s_from, s_to) - bot.delete_message(message.chat_id, message.message_id) - ctx.reply('%.2f Wh' % (wh,), - markup=bot.IgnoreMarkup()) + await bot.delete_message(message.chat_id, message.message_id) + await ctx.reply('%.2f Wh' % (wh,), + markup=bot.IgnoreMarkup()) return self.END except Exception as e: - bot.delete_message(message.chat_id, message.message_id) - ctx.reply_exc(e) + await asyncio.gather( + bot.delete_message(message.chat_id, message.message_id), + ctx.reply_exc(e) + ) # other # ----- @bot.handler(command='monstatus') -def monstatus_handler(ctx: bot.Context) -> None: +async def monstatus_handler(ctx: bot.Context) -> None: msg = '' st = monitor.dump_status() for k, v in st.items(): msg += k + ': ' + str(v) + '\n' - ctx.reply(msg) + await ctx.reply(msg) @bot.handler(command='monsetcur') -def monsetcur_handler(ctx: bot.Context) -> None: - ctx.reply('not implemented yet') +async def monsetcur_handler(ctx: bot.Context) -> None: + await ctx.reply('not implemented yet') @bot.callbackhandler -def button_callback(ctx: bot.Context) -> None: +async def button_callback(ctx: bot.Context) -> None: query = ctx.callback_query if query.data.startswith('flag_'): @@ -762,7 +782,7 @@ def button_callback(ctx: bot.Context) -> None: json_key = k break if not found: - query.answer(ctx.lang('flags_invalid')) + await query.answer(ctx.lang('flags_invalid')) return flags = inverter.exec('get-flags')['data'] @@ -773,32 +793,31 @@ def button_callback(ctx: bot.Context) -> None: response = inverter.exec('set-flag', (flag, target_flag_value)) # notify user - query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) + await query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) # edit message flags[json_key] = not cur_flag_value text, markup = build_flags_keyboard(flags, ctx) - query.edit_message_text(text, reply_markup=markup) + await query.edit_message_text(text, reply_markup=markup) else: - query.answer(ctx.lang('unexpected_callback_data')) + await query.answer(ctx.lang('unexpected_callback_data')) @bot.exceptionhandler -def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: +async def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: if isinstance(e, InverterError): try: err = json.loads(str(e))['message'] except json.decoder.JSONDecodeError: err = str(e) err = re.sub(r'((?:.*)?error:) (.*)', r'\1 \2', err) - ctx.reply(err, - markup=bot.IgnoreMarkup()) + await ctx.reply(err, markup=bot.IgnoreMarkup()) return True @bot.handler(message='status') -def status_handler(ctx: bot.Context) -> None: +async def status_handler(ctx: bot.Context) -> None: gs = inverter.exec('get-status')['data'] rated = inverter.exec('get-rated')['data'] @@ -842,11 +861,11 @@ def status_handler(ctx: bot.Context) -> None: html += f'\n{ctx.lang("priority")}: {rated["output_source_priority"]}' # send response - ctx.reply(html) + await ctx.reply(html) @bot.handler(message='generation') -def generation_handler(ctx: bot.Context) -> None: +async def generation_handler(ctx: bot.Context) -> None: today = datetime.date.today() yday = today - datetime.timedelta(days=1) yday2 = today - datetime.timedelta(days=2) @@ -876,7 +895,7 @@ def generation_handler(ctx: bot.Context) -> None: html += f'\n{ctx.lang("yday2")}: %s Wh' % (gen_yday2['wh']) # send response - ctx.reply(html) + await ctx.reply(html) @bot.defaultreplymarkup diff --git a/include/py/homekit/inverter/monitor.py b/include/py/homekit/inverter/monitor.py index 86f75ac..5955d92 100644 --- a/include/py/homekit/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/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index 8a78c6f..2efd9e4 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -274,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}') @@ -362,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 = [] @@ -400,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 From 387c26e218f7bf10819d7bed657f7f62b64e18ce Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 03:30:12 +0300 Subject: [PATCH 19/51] move some scripts around, delete obsolete ones --- {tools => bin}/ipcam_capture.sh | 2 +- {tools => bin}/ipcam_motion_worker.sh | 2 +- {tools => bin}/ipcam_rtsp2hls.sh | 0 tools/lib.bash => include/bash/include.bash | 0 .../homekit_ipcam_capture_restart.sh} | 0 .../homekit_ipcam_rtsp2hls_restart.sh} | 0 .../homekit_make_netns_per_upstream.sh} | 0 .../homekit_sunxi_h3_i2c_reset.sh | 0 .../homekit_sunxi_setup_amixer.sh | 0 .../homekit_sync_recordings_to_remote.sh | 0 .../remote_server/clickhouse_backup.sh | 0 .../remote_server/remove_old_recordings.sh | 0 systemd/ipcam_capture@.service | 2 +- systemd/ipcam_rtsp2hls@.service | 2 +- tools/process-motion-timecodes.py | 61 ------------------- tools/rotate-video.sh | 2 +- tools/video-util.sh | 2 +- 17 files changed, 6 insertions(+), 67 deletions(-) rename {tools => bin}/ipcam_capture.sh (99%) rename {tools => bin}/ipcam_motion_worker.sh (99%) rename {tools => bin}/ipcam_rtsp2hls.sh (100%) rename tools/lib.bash => include/bash/include.bash (100%) rename misc/scripts/{ipcam_capture_restart.sh => home_linux_boards/homekit_ipcam_capture_restart.sh} (100%) rename misc/scripts/{ipcam_rtsp2hls_restart.sh => home_linux_boards/homekit_ipcam_rtsp2hls_restart.sh} (100%) rename misc/scripts/{make_netns_per_upstream.sh => home_linux_boards/homekit_make_netns_per_upstream.sh} (100%) rename tools/sunxi-h3-i2c-reset.sh => misc/scripts/home_linux_boards/homekit_sunxi_h3_i2c_reset.sh (100%) rename tools/sunxi-setup-amixer.sh => misc/scripts/home_linux_boards/homekit_sunxi_setup_amixer.sh (100%) rename tools/sync-recordings-to-remote.sh => misc/scripts/home_linux_boards/homekit_sync_recordings_to_remote.sh (100%) rename tools/clickhouse-backup.sh => misc/scripts/remote_server/clickhouse_backup.sh (100%) rename tools/remove-old-recordings.sh => misc/scripts/remote_server/remove_old_recordings.sh (100%) delete mode 100755 tools/process-motion-timecodes.py diff --git a/tools/ipcam_capture.sh b/bin/ipcam_capture.sh similarity index 99% rename from tools/ipcam_capture.sh rename to bin/ipcam_capture.sh index 08b9093..b97c856 100755 --- a/tools/ipcam_capture.sh +++ b/bin/ipcam_capture.sh @@ -36,7 +36,7 @@ EOF validate_channel() { local c="$1" case "$c" in - 1 | 2) + 1|2) : ;; *) diff --git a/tools/ipcam_motion_worker.sh b/bin/ipcam_motion_worker.sh similarity index 99% rename from tools/ipcam_motion_worker.sh rename to 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/tools/ipcam_rtsp2hls.sh b/bin/ipcam_rtsp2hls.sh similarity index 100% rename from tools/ipcam_rtsp2hls.sh rename to bin/ipcam_rtsp2hls.sh diff --git a/tools/lib.bash b/include/bash/include.bash similarity index 100% rename from tools/lib.bash rename to include/bash/include.bash diff --git a/misc/scripts/ipcam_capture_restart.sh b/misc/scripts/home_linux_boards/homekit_ipcam_capture_restart.sh similarity index 100% rename from misc/scripts/ipcam_capture_restart.sh rename to misc/scripts/home_linux_boards/homekit_ipcam_capture_restart.sh diff --git a/misc/scripts/ipcam_rtsp2hls_restart.sh b/misc/scripts/home_linux_boards/homekit_ipcam_rtsp2hls_restart.sh similarity index 100% rename from misc/scripts/ipcam_rtsp2hls_restart.sh rename to misc/scripts/home_linux_boards/homekit_ipcam_rtsp2hls_restart.sh diff --git a/misc/scripts/make_netns_per_upstream.sh b/misc/scripts/home_linux_boards/homekit_make_netns_per_upstream.sh similarity index 100% rename from misc/scripts/make_netns_per_upstream.sh rename to misc/scripts/home_linux_boards/homekit_make_netns_per_upstream.sh diff --git a/tools/sunxi-h3-i2c-reset.sh b/misc/scripts/home_linux_boards/homekit_sunxi_h3_i2c_reset.sh similarity index 100% rename from tools/sunxi-h3-i2c-reset.sh rename to misc/scripts/home_linux_boards/homekit_sunxi_h3_i2c_reset.sh diff --git a/tools/sunxi-setup-amixer.sh b/misc/scripts/home_linux_boards/homekit_sunxi_setup_amixer.sh similarity index 100% rename from tools/sunxi-setup-amixer.sh rename to misc/scripts/home_linux_boards/homekit_sunxi_setup_amixer.sh diff --git a/tools/sync-recordings-to-remote.sh b/misc/scripts/home_linux_boards/homekit_sync_recordings_to_remote.sh similarity index 100% rename from tools/sync-recordings-to-remote.sh rename to misc/scripts/home_linux_boards/homekit_sync_recordings_to_remote.sh diff --git a/tools/clickhouse-backup.sh b/misc/scripts/remote_server/clickhouse_backup.sh similarity index 100% rename from tools/clickhouse-backup.sh rename to misc/scripts/remote_server/clickhouse_backup.sh diff --git a/tools/remove-old-recordings.sh b/misc/scripts/remote_server/remove_old_recordings.sh similarity index 100% rename from tools/remove-old-recordings.sh rename to misc/scripts/remote_server/remove_old_recordings.sh diff --git a/systemd/ipcam_capture@.service b/systemd/ipcam_capture@.service index b1c363e..e195231 100644 --- a/systemd/ipcam_capture@.service +++ b/systemd/ipcam_capture@.service @@ -8,7 +8,7 @@ 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 +ExecStart=/home/user/homekit/bin/ipcam_capture.sh --outdir $OUTDIR --creds $CREDS --ip $IP --port $PORT $ARGS Restart=always [Install] diff --git a/systemd/ipcam_rtsp2hls@.service b/systemd/ipcam_rtsp2hls@.service index efcdd6a..9ce6cca 100644 --- a/systemd/ipcam_rtsp2hls@.service +++ b/systemd/ipcam_rtsp2hls@.service @@ -8,7 +8,7 @@ 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 +ExecStart=/home/user/homekit/bin/ipcam_rtsp2hls.sh --name %i --user $USER --password $PASSWORD --ip $IP --port $PORT $ARGS Restart=on-failure RestartSec=3 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= From 58d6d519d104196314c02b77cdd05cd996f71508 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 03:32:21 +0300 Subject: [PATCH 20/51] start splitting requirements.txt into multiple files --- requirements.txt | 6 +----- requirements_kettle.txt | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 requirements_kettle.txt diff --git a/requirements.txt b/requirements.txt index 4595dea..521ae41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,8 +17,4 @@ cerberus~=1.3.4 # following can be installed from debian repositories # matplotlib~=3.5.0 -Pillow==9.5.0 - -# for polaris kettle protocol implementation -cryptography==41.0.1 -zeroconf==0.64.1 \ 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 From ba321657e0e724082df206857f80ca08c4d999dc Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 04:29:14 +0300 Subject: [PATCH 21/51] misc/scripts: reorganize files --- .../usr/local/bin}/homekit_ipcam_capture_restart.sh | 0 .../usr/local/bin}/homekit_ipcam_rtsp2hls_restart.sh | 0 .../usr/local/bin}/homekit_make_netns_per_upstream.sh | 0 .../usr/local/bin}/homekit_sunxi_h3_i2c_reset.sh | 0 .../usr/local/bin}/homekit_sunxi_setup_amixer.sh | 0 .../usr/local/bin}/homekit_sync_recordings_to_remote.sh | 0 .../usr/local/bin}/clickhouse_backup.sh | 0 .../usr/local/bin}/remove_old_recordings.sh | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_ipcam_capture_restart.sh (100%) mode change 100644 => 100755 rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_ipcam_rtsp2hls_restart.sh (100%) mode change 100644 => 100755 rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_make_netns_per_upstream.sh (100%) mode change 100644 => 100755 rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_sunxi_h3_i2c_reset.sh (100%) mode change 100644 => 100755 rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_sunxi_setup_amixer.sh (100%) rename misc/{scripts/home_linux_boards => home_linux_boards/usr/local/bin}/homekit_sync_recordings_to_remote.sh (100%) rename misc/{scripts/remote_server => remote_server/usr/local/bin}/clickhouse_backup.sh (100%) mode change 100644 => 100755 rename misc/{scripts/remote_server => remote_server/usr/local/bin}/remove_old_recordings.sh (100%) mode change 100644 => 100755 diff --git a/misc/scripts/home_linux_boards/homekit_ipcam_capture_restart.sh b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_capture_restart.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/home_linux_boards/homekit_ipcam_capture_restart.sh rename to misc/home_linux_boards/usr/local/bin/homekit_ipcam_capture_restart.sh diff --git a/misc/scripts/home_linux_boards/homekit_ipcam_rtsp2hls_restart.sh b/misc/home_linux_boards/usr/local/bin/homekit_ipcam_rtsp2hls_restart.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/home_linux_boards/homekit_ipcam_rtsp2hls_restart.sh rename to misc/home_linux_boards/usr/local/bin/homekit_ipcam_rtsp2hls_restart.sh diff --git a/misc/scripts/home_linux_boards/homekit_make_netns_per_upstream.sh b/misc/home_linux_boards/usr/local/bin/homekit_make_netns_per_upstream.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/home_linux_boards/homekit_make_netns_per_upstream.sh rename to misc/home_linux_boards/usr/local/bin/homekit_make_netns_per_upstream.sh diff --git a/misc/scripts/home_linux_boards/homekit_sunxi_h3_i2c_reset.sh b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_h3_i2c_reset.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/home_linux_boards/homekit_sunxi_h3_i2c_reset.sh rename to misc/home_linux_boards/usr/local/bin/homekit_sunxi_h3_i2c_reset.sh diff --git a/misc/scripts/home_linux_boards/homekit_sunxi_setup_amixer.sh b/misc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh similarity index 100% rename from misc/scripts/home_linux_boards/homekit_sunxi_setup_amixer.sh rename to misc/home_linux_boards/usr/local/bin/homekit_sunxi_setup_amixer.sh diff --git a/misc/scripts/home_linux_boards/homekit_sync_recordings_to_remote.sh b/misc/home_linux_boards/usr/local/bin/homekit_sync_recordings_to_remote.sh similarity index 100% rename from misc/scripts/home_linux_boards/homekit_sync_recordings_to_remote.sh rename to misc/home_linux_boards/usr/local/bin/homekit_sync_recordings_to_remote.sh diff --git a/misc/scripts/remote_server/clickhouse_backup.sh b/misc/remote_server/usr/local/bin/clickhouse_backup.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/remote_server/clickhouse_backup.sh rename to misc/remote_server/usr/local/bin/clickhouse_backup.sh diff --git a/misc/scripts/remote_server/remove_old_recordings.sh b/misc/remote_server/usr/local/bin/remove_old_recordings.sh old mode 100644 new mode 100755 similarity index 100% rename from misc/scripts/remote_server/remove_old_recordings.sh rename to misc/remote_server/usr/local/bin/remove_old_recordings.sh From 62ee71fdb0eb07adbf0071103617aa96c993fe22 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:03:43 +0300 Subject: [PATCH 22/51] ipcam: start porting to new config and multiserver scheme --- .gitignore | 2 +- bin/ipcam_server.py | 16 ++-- include/py/homekit/camera/config.py | 82 +++++++++++++++++++ include/py/homekit/camera/types.py | 12 +++ include/py/homekit/config/config.py | 3 + include/py/homekit/database/sqlite.py | 11 ++- .../etc/default/homekit_ipcam_server | 2 + systemd/ipcam_server.service | 5 +- test/test.py | 8 +- 9 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 include/py/homekit/camera/config.py create mode 100644 misc/home_linux_boards/etc/default/homekit_ipcam_server diff --git a/.gitignore b/.gitignore index 9a32ecc..b113ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ __pycache__ /include/test/test_inverter_monitor.log /youtrack-certificate /cpp -/include/test.py +/test/test.py /bin/test.py /arduino/ESP32CameraWebServer/wifi_password.h cmake-build-* diff --git a/bin/ipcam_server.py b/bin/ipcam_server.py index 211bc86..a9d6a0b 100755 --- a/bin/ipcam_server.py +++ b/bin/ipcam_server.py @@ -9,6 +9,7 @@ import __py_include import homekit.telegram.aio as telegram +from argparse import ArgumentParser from apscheduler.schedulers.asyncio import AsyncIOScheduler from asyncio import Lock @@ -53,8 +54,8 @@ def get_all_cams() -> list: 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() @@ -319,9 +320,9 @@ 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() @@ -558,9 +559,12 @@ logger = logging.getLogger(__name__) # -------------------- if __name__ == '__main__': - config.load_app('ipcam_server') + parser = ArgumentParser() + parser.add_argument('--listen', type=str, required=True) + parser.add_argument('--database-path', type=str, required=True) + arg = config.load_app(no_config=True, parser=parser) - open_database() + open_database(arg.database_path) loop = asyncio.get_event_loop() diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py new file mode 100644 index 0000000..e0891a6 --- /dev/null +++ b/include/py/homekit/camera/config.py @@ -0,0 +1,82 @@ +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]: + lbc = LinuxBoardsConfig() + 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} + } + } + } + } + } + }, + '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() + } + + @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'} + } + } \ No newline at end of file diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index de59022..0d3a384 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -3,3 +3,15 @@ from enum import Enum class CameraType(Enum): ESP32 = 'esp32' + ALIEXPRESS_NONAME = 'ali' + HIKVISION = 'hik' + + +class VideoContainerType(Enum): + MP4 = 'mp4' + MOV = 'mov' + + +class VideoCodecType(Enum): + H264 = 'h264' + H265 = 'h265' diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 7344386..f2a3990 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -158,6 +158,9 @@ class ConfigUnit(BaseConfigUnit): else: normalized = v.validated(self._data, schema) + if not normalized: + raise cerberus.DocumentError(f'validation failed: {v.errors}') + self._data = normalized try: diff --git a/include/py/homekit/database/sqlite.py b/include/py/homekit/database/sqlite.py index 0af1f54..8b0c44c 100644 --- a/include/py/homekit/database/sqlite.py +++ b/include/py/homekit/database/sqlite.py @@ -15,10 +15,13 @@ def _get_database_path(name: str) -> str: class SQLiteBase: SCHEMA = 1 - def __init__(self, name=None, check_same_thread=False): - if name is None: - name = config.app_config['database_name'] - database_path = _get_database_path(name) + def __init__(self, name=None, path=None, check_same_thread=False): + if not path: + if not name: + name = config.app_config['database_name'] + 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)) 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/systemd/ipcam_server.service b/systemd/ipcam_server.service index e6f8918..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/bin/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/test/test.py b/test/test.py index 267a19f..0c4a347 100755 --- a/test/test.py +++ b/test/test.py @@ -1,8 +1,10 @@ #!/usr/bin/env python import __py_include -from homekit.relay import RelayClient + +from pprint import pprint +from homekit.camera.config import IpcamConfig if __name__ == '__main__': - c = RelayClient() - print(c, c._host) \ No newline at end of file + c = IpcamConfig() + pprint(c.get()) \ No newline at end of file From 26bd30dff41f5f0e3857283155362a96c47ab9bb Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:04:41 +0300 Subject: [PATCH 23/51] minor fix --- include/py/homekit/camera/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index e0891a6..331e595 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -21,7 +21,6 @@ class IpcamConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: - lbc = LinuxBoardsConfig() return { 'cams': { 'type': 'dict', @@ -32,7 +31,7 @@ class IpcamConfig(ConfigUnit): '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}, + 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True}, 'disk': {'type': 'integer', 'required': True}, 'motion': { 'type': 'dict', From 3da04de6fd83bca19447a865bf84b3403a14e0c1 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:06:28 +0300 Subject: [PATCH 24/51] ipcam/config: fix schema validation --- include/py/homekit/camera/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 331e595..0d4c747 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -65,7 +65,7 @@ class IpcamConfig(ConfigUnit): 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']): + if disk < 1 or disk >= len(linux_box['ext_hdd']): raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') @classmethod From 08e736c48990bec0423f72cf52dd1a457e9f9590 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:06:47 +0300 Subject: [PATCH 25/51] Revert "ipcam/config: fix schema validation" This reverts commit 3da04de6fd83bca19447a865bf84b3403a14e0c1. --- include/py/homekit/camera/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 0d4c747..331e595 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -65,7 +65,7 @@ class IpcamConfig(ConfigUnit): 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 < 1 or disk >= len(linux_box['ext_hdd']): + if disk < 0 or disk >= len(linux_box['ext_hdd']): raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') @classmethod From cbb6ad451749c54039e6d7c7eeeb5e387d95c069 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:10:21 +0300 Subject: [PATCH 26/51] typo --- bin/pio_ini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pio_ini.py b/bin/pio_ini.py index 7254eca..ee85732 100755 --- a/bin/pio_ini.py +++ b/bin/pio_ini.py @@ -109,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') From 58b5a1b5fca1cd898b1121778a3205ce2dafae36 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 14:02:31 +0300 Subject: [PATCH 27/51] delete test/test.py --- test/test.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100755 test/test.py diff --git a/test/test.py b/test/test.py deleted file mode 100755 index 0c4a347..0000000 --- a/test/test.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import __py_include - -from pprint import pprint -from homekit.camera.config import IpcamConfig - - -if __name__ == '__main__': - c = IpcamConfig() - pprint(c.get()) \ No newline at end of file From 5d8e81b6c8fc7abe75188007c6a86bb501a314ad Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 14:02:47 +0300 Subject: [PATCH 28/51] config: turn ConfigUnit into singleton --- include/py/homekit/config/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index f2a3990..29364af 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -76,6 +76,13 @@ class BaseConfigUnit(ABC): class ConfigUnit(BaseConfigUnit): NAME = 'dumb' + _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__() From e97f98e5e27a6df3827564cce594f27f18c89267 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 14 Jun 2023 14:06:26 +0300 Subject: [PATCH 29/51] wip --- bin/ipcam_capture.py | 141 ++++++++++++++++++ bin/ipcam_capture.sh | 119 --------------- bin/ipcam_rtsp2hls.sh | 127 ---------------- bin/ipcam_server.py | 203 ++++++++++---------------- bin/web_api.py | 1 - include/py/homekit/audio/amixer.py | 14 +- include/py/homekit/camera/__init__.py | 3 +- include/py/homekit/camera/config.py | 57 +++++++- include/py/homekit/camera/types.py | 29 ++++ include/py/homekit/camera/util.py | 70 ++++++++- include/py/homekit/config/_configs.py | 6 + systemd/ipcam_capture@.service | 15 -- systemd/ipcam_rtsp2hls@.service | 16 -- 13 files changed, 385 insertions(+), 416 deletions(-) create mode 100755 bin/ipcam_capture.py delete mode 100755 bin/ipcam_capture.sh delete mode 100755 bin/ipcam_rtsp2hls.sh delete mode 100644 systemd/ipcam_capture@.service delete mode 100644 systemd/ipcam_rtsp2hls@.service 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/bin/ipcam_capture.sh b/bin/ipcam_capture.sh deleted file mode 100755 index b97c856..0000000 --- a/bin/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 <&2 "error: $@" - exit 1 -} - -usage() { - cat < 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, path=None): @@ -67,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: @@ -135,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) @@ -146,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() @@ -173,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}) @@ -188,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(): @@ -200,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() @@ -216,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 != '': @@ -239,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() @@ -280,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'])) @@ -307,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: @@ -322,30 +296,22 @@ class IPCamWebServer(http.HTTPServer): def open_database(database_path: str): global db - db = IPCamServerDatabase(database_path) + 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: @@ -362,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: @@ -382,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) @@ -392,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 @@ -408,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: {camera}\n' @@ -423,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 @@ -446,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'{link_name}') return ' '.join(links) @@ -462,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}') @@ -473,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) @@ -482,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 @@ -516,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: @@ -537,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") @@ -550,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__) @@ -562,7 +517,7 @@ if __name__ == '__main__': parser = ArgumentParser() parser.add_argument('--listen', type=str, required=True) parser.add_argument('--database-path', type=str, required=True) - arg = config.load_app(no_config=True, parser=parser) + arg = homekit_config.load_app(no_config=True, parser=parser) open_database(arg.database_path) @@ -570,10 +525,14 @@ if __name__ == '__main__': try: scheduler = AsyncIOScheduler(event_loop=loop) - if config['fix_enabled']: - scheduler.add_job(fix_job, 'interval', seconds=config['fix_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=config['cleanup_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 @@ -581,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/web_api.py b/bin/web_api.py index e543d22..d221838 100755 --- a/bin/web_api.py +++ b/bin/web_api.py @@ -42,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) diff --git a/include/py/homekit/audio/amixer.py b/include/py/homekit/audio/amixer.py index 5133c97..8ed754b 100644 --- a/include/py/homekit/audio/amixer.py +++ b/include/py/homekit/audio/amixer.py @@ -1,6 +1,6 @@ import subprocess -from ..config import app_config as config +from ..config import config from threading import Lock from typing import Union, List @@ -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 index 626930b..4875031 100644 --- a/include/py/homekit/camera/__init__.py +++ b/include/py/homekit/camera/__init__.py @@ -1 +1,2 @@ -from .types import CameraType \ No newline at end of file +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 index 331e595..c7dbc38 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -1,8 +1,9 @@ +import socket + from ..config import ConfigUnit, LinuxBoardsConfig from typing import Optional from .types import CameraType, VideoContainerType, VideoCodecType - _lbc = LinuxBoardsConfig() @@ -42,7 +43,8 @@ class IpcamConfig(ConfigUnit): 'schema': {'type': 'string', 'check_with': _validate_roi_line} } } - } + }, + 'rtsp_tcp': {'type': 'boolean'} } } }, @@ -55,7 +57,19 @@ class IpcamConfig(ConfigUnit): # TODO FIXME 'fragment_url_templates': cls._url_templates_schema(), - 'original_file_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 @@ -78,4 +92,39 @@ class IpcamConfig(ConfigUnit): 'empty': False, 'schema': {'type': 'string'} } - } \ No newline at end of file + } + + 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/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index 0d3a384..c313b58 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -6,6 +6,19 @@ class CameraType(Enum): 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' @@ -15,3 +28,19 @@ class VideoContainerType(Enum): 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/include/py/homekit/camera/util.py b/include/py/homekit/camera/util.py index 97f35aa..58c2c70 100644 --- a/include/py/homekit/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/_configs.py b/include/py/homekit/config/_configs.py index 1628cba..f88c8ea 100644 --- a/include/py/homekit/config/_configs.py +++ b/include/py/homekit/config/_configs.py @@ -53,3 +53,9 @@ class LinuxBoardsConfig(ConfigUnit): }, } } + + def get_board_disks(self, name: str) -> list[dict]: + return self[name]['ext_hdd'] + + def get_board_disks_count(self, name: str) -> int: + return len(self[name]['ext_hdd']) diff --git a/systemd/ipcam_capture@.service b/systemd/ipcam_capture@.service deleted file mode 100644 index e195231..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/bin/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 9ce6cca..0000000 --- a/systemd/ipcam_rtsp2hls@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=convert rtsp to hls for viewing live camera feeds in browser -After=network-online.target - -[Service] -Restart=always -RestartSec=3 -User=user -Group=user -EnvironmentFile=/etc/ipcam_rtsp2hls.conf.d/%i.conf -ExecStart=/home/user/homekit/bin/ipcam_rtsp2hls.sh --name %i --user $USER --password $PASSWORD --ip $IP --port $PORT $ARGS -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=multi-user.target From 94afba2bb100504c19c271ea10ae7a95058d3e08 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 7 Sep 2023 00:38:21 +0300 Subject: [PATCH 30/51] mqtt_node_util: add --legacy-relay option --- bin/mqtt_node_util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py index 420a87e..cf451fd 100755 --- a/bin/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -23,6 +23,7 @@ if __name__ == '__main__': 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') @@ -45,7 +46,10 @@ if __name__ == '__main__': if arg.modules: for m in arg.modules: - module_instance = mqtt_node.load_module(m) + 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) From 6994741c612e74a28683ca7cdd7b14f9876a0305 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 7 Sep 2023 00:38:34 +0300 Subject: [PATCH 31/51] mqtt: fix cacert path --- include/py/homekit/mqtt/_mqtt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/include/py/homekit/mqtt/_mqtt.py b/include/py/homekit/mqtt/_mqtt.py index fb35a24..47ee9ae 100644 --- a/include/py/homekit/mqtt/_mqtt.py +++ b/include/py/homekit/mqtt/_mqtt.py @@ -45,6 +45,7 @@ class Mqtt: '..', '..', '..', + '..', 'misc', 'mqtt_ca.crt' )) From 949eec3dc9cd37c70fb553e3e3f57decc8c89afc Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 7 Sep 2023 01:32:21 +0300 Subject: [PATCH 32/51] ConfigUnit: fix static class variable inheritance --- include/py/homekit/config/config.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 29364af..773de1e 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -52,6 +52,8 @@ class BaseConfigUnit(ABC): def load_from(self, path: str): with open(path, 'r') as fd: self._data = yaml.safe_load(fd) + if self._data is None: + raise TypeError(f'config file {path} is empty') def get(self, key: Optional[str] = None, @@ -78,6 +80,10 @@ class ConfigUnit(BaseConfigUnit): _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) @@ -200,7 +206,7 @@ class AppConfigUnit(ConfigUnit): def logging_get_fmt(self) -> Optional[str]: try: return self['logging']['default_fmt'] - except KeyError: + except (KeyError, TypeError): return self._logging_fmt def logging_set_file(self, file: str) -> None: @@ -209,7 +215,7 @@ class AppConfigUnit(ConfigUnit): def logging_get_file(self) -> Optional[str]: try: return self['logging']['file'] - except KeyError: + except (KeyError, TypeError): return self._logging_file def logging_set_verbose(self): @@ -218,7 +224,7 @@ class AppConfigUnit(ConfigUnit): def logging_is_verbose(self) -> bool: try: return bool(self['logging']['verbose']) - except KeyError: + except (KeyError, TypeError): return self._logging_verbose @@ -271,7 +277,9 @@ class Config: 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 From 44aad914a3cea1b6e39cf5db7bebeafb59191707 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 7 Sep 2023 01:32:58 +0300 Subject: [PATCH 33/51] util: Addr.fromstring(): minor rcode style fix --- include/py/homekit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 11e7116..22bba86 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -60,7 +60,7 @@ class Addr: if not colons: host = addr - port= None + port = None else: host, port = addr.split(':') From 405a17a9fdd420faa7af90f769e72eb21fda73ce Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 13 Sep 2023 09:34:49 +0300 Subject: [PATCH 34/51] save --- bin/web_kbn.py | 58 +++++++++++++++++++++++++++++ include/py/homekit/config/config.py | 2 - include/py/homekit/util.py | 9 ++++- requirements.txt | 5 ++- web/kbn_templates/base.html | 23 ++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 bin/web_kbn.py create mode 100644 web/kbn_templates/base.html diff --git a/bin/web_kbn.py b/bin/web_kbn.py new file mode 100644 index 0000000..b66e2a5 --- /dev/null +++ b/bin/web_kbn.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import asyncio +import jinja2 +import aiohttp_jinja2 +import os +import __py_include + +from typing import Optional +from homekit.config import config, AppConfigUnit +from homekit.util import homekit_path +from aiohttp import web +from homekit import http + + +class WebKbnConfig(AppConfigUnit): + NAME = 'web_kbn' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'listen_addr': cls._addr_schema(required=True) + } + + +class WebSite(http.HTTPServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + aiohttp_jinja2.setup( + self.app, + loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')) + ) + + self.get('/', self.get_index) + + @staticmethod + async def get_index(req: http.Request): + # context = { + # 'username': request.match_info.get("username", ""), + # 'current_date': 'January 27, 2017' + # } + # response = aiohttp_jinja2.render_template("example.html", request, + # context=context) + # return response + + message = "nothing here, keep lurking" + return http.Response(text=message, content_type='text/plain') + + +if __name__ == '__main__': + config.load_app(WebKbnConfig) + + loop = asyncio.get_event_loop() + # print(config.app_config) + + print(config.app_config['listen_addr'].host) + server = WebSite(config.app_config['listen_addr']) + server.run() diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 773de1e..7d30a77 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -277,9 +277,7 @@ class Config: 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 diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 22bba86..2680c37 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -9,6 +9,7 @@ import logging import string import random import re +import os from enum import Enum from datetime import datetime @@ -252,4 +253,10 @@ def next_tick_gen(freq): t = time.time() while True: t += freq - yield max(t - time.time(), 0) \ No newline at end of file + yield max(t - time.time(), 0) + + +def homekit_path(*args) -> str: + return os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', *args) + ) diff --git a/requirements.txt b/requirements.txt index 521ae41..66e8379 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,7 @@ cerberus~=1.3.4 # following can be installed from debian repositories # matplotlib~=3.5.0 -Pillow==9.5.0 \ No newline at end of file +Pillow==9.5.0 + +jinja2~=3.1.2 +aiohttp-jinja2~=1.5.1 \ No newline at end of file diff --git a/web/kbn_templates/base.html b/web/kbn_templates/base.html new file mode 100644 index 0000000..e567a90 --- /dev/null +++ b/web/kbn_templates/base.html @@ -0,0 +1,23 @@ + + + + {{ title }} + + + + {{ head_static }} + + +
+ +{% if js %} + +{% endif %} + +
+ + From a32e4a1629a20026c364059c7bbaec1dbd64353b Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 17 Sep 2023 04:38:12 +0300 Subject: [PATCH 35/51] multiple fixes --- include/py/homekit/config/config.py | 3 ++- include/py/homekit/database/_base.py | 2 +- include/py/homekit/database/sqlite.py | 2 +- include/py/homekit/telegram/bot.py | 2 +- include/py/homekit/telegram/config.py | 11 +++++++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 773de1e..5fe1ae8 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -10,6 +10,7 @@ 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): @@ -140,7 +141,7 @@ class ConfigUnit(BaseConfigUnit): schema['logging'] = { 'type': 'dict', 'schema': { - 'logging': {'type': 'boolean'} + 'verbose': {'type': 'boolean'} } } diff --git a/include/py/homekit/database/_base.py b/include/py/homekit/database/_base.py index c01e62b..dcec9da 100644 --- a/include/py/homekit/database/_base.py +++ b/include/py/homekit/database/_base.py @@ -1,7 +1,7 @@ import os -def get_data_root_directory(name: str) -> str: +def get_data_root_directory() -> str: return os.path.join( os.environ['HOME'], '.config', diff --git a/include/py/homekit/database/sqlite.py b/include/py/homekit/database/sqlite.py index 8b0c44c..1651a93 100644 --- a/include/py/homekit/database/sqlite.py +++ b/include/py/homekit/database/sqlite.py @@ -18,7 +18,7 @@ class SQLiteBase: def __init__(self, name=None, path=None, check_same_thread=False): if not path: if not name: - name = config.app_config['database_name'] + name = config.app_name database_path = _get_database_path(name) else: database_path = path diff --git a/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index 2efd9e4..f5f620a 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -266,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) diff --git a/include/py/homekit/telegram/config.py b/include/py/homekit/telegram/config.py index 4c7d74b..5f41008 100644 --- a/include/py/homekit/telegram/config.py +++ b/include/py/homekit/telegram/config.py @@ -51,15 +51,15 @@ class TelegramBotConfig(ConfigUnit, ABC): 'type': 'dict', 'schema': { 'token': {'type': 'string', 'required': True}, - TelegramUserListType.USERS: {**TelegramBotConfig._userlist_schema(), 'required': True}, - TelegramUserListType.NOTIFY: TelegramBotConfig._userlist_schema(), + TelegramUserListType.USERS.value: {**TelegramBotConfig._userlist_schema(), 'required': True}, + TelegramUserListType.NOTIFY.value: TelegramBotConfig._userlist_schema(), } } } @staticmethod def _userlist_schema() -> dict: - return {'type': 'list', 'schema': {'type': ['string', 'int']}} + return {'type': 'list', 'schema': {'type': ['string', 'integer']}} @staticmethod def custom_validator(data): @@ -72,4 +72,7 @@ class TelegramBotConfig(ConfigUnit, ABC): def get_user_ids(self, ult: TelegramUserListType = TelegramUserListType.USERS) -> list[int]: - return list(map(_user_id_mapper, self['bot'][ult.value])) \ No newline at end of file + try: + return list(map(_user_id_mapper, self['bot'][ult.value])) + except KeyError: + return [] \ No newline at end of file From 9b78ccca3546f93955571f4a20a44a1739e718b8 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 17 Sep 2023 04:38:26 +0300 Subject: [PATCH 36/51] bin: add lugobaya_pump_mqtt_bot test app --- bin/lugovaya_pump_mqtt_bot.py | 201 ++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100755 bin/lugovaya_pump_mqtt_bot.py diff --git a/bin/lugovaya_pump_mqtt_bot.py b/bin/lugovaya_pump_mqtt_bot.py new file mode 100755 index 0000000..72a2e87 --- /dev/null +++ b/bin/lugovaya_pump_mqtt_bot.py @@ -0,0 +1,201 @@ +#!/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='Пользователь %s %s насос.', + 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 %s turned the pump %s.', + 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() From bdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 17 Sep 2023 04:48:05 +0300 Subject: [PATCH 37/51] fix --- bin/lugovaya_pump_mqtt_bot.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/lugovaya_pump_mqtt_bot.py b/bin/lugovaya_pump_mqtt_bot.py index 72a2e87..85402d1 100755 --- a/bin/lugovaya_pump_mqtt_bot.py +++ b/bin/lugovaya_pump_mqtt_bot.py @@ -173,7 +173,13 @@ def exception_handler(e: Exception, ctx: bot.Context) -> bool: @bot.defaultreplymarkup def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: - buttons = [[ctx.lang('enable'), ctx.lang('disable')], [ctx.lang('status')]] + 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) @@ -191,7 +197,7 @@ try: except KeyError: pass relay_module = mqtt_node.load_module('relay', **module_kwargs) -mqtt_node.add_payload_callback(on_mqtt_message) +# mqtt_node.add_payload_callback(on_mqtt_message) mqtt.add_node(mqtt_node) mqtt.connect_and_loop(loop_forever=False) From 3623e770b6b25fcaa1c8d76b9d3dafefec480876 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 24 Sep 2023 02:49:12 +0300 Subject: [PATCH 38/51] mqtt: various fixes --- bin/mqtt_node_util.py | 51 ++++++++++++++++++++----- include/py/homekit/mqtt/_config.py | 2 +- include/py/homekit/mqtt/_wrapper.py | 21 ++++++++++ include/py/homekit/mqtt/module/relay.py | 3 +- include/py/homekit/pio/products.py | 3 ++ 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py index cf451fd..c1d457c 100755 --- a/bin/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -7,12 +7,37 @@ 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 +from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules, MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayModule +from homekit.mqtt.module.ota import MqttOtaModule mqtt_node: Optional[MqttNode] = None mqtt: Optional[MqttWrapper] = None +relay_module: Optional[MqttOtaModule] = None +relay_val = None + +ota_module: Optional[MqttRelayModule] = None +ota_val = False + +no_wait = False +stop_loop = False + + +def on_mqtt_connect(): + global stop_loop + + if relay_module: + relay_module.switchpower(relay_val == 1) + + if ota_val: + if not os.path.exists(arg.push_ota): + raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') + ota_module.push_ota(arg.push_ota, 1) + + if no_wait: + stop_loop = True + if __name__ == '__main__': nodes_config = MqttNodesConfig() @@ -26,15 +51,21 @@ if __name__ == '__main__': 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') + parser.add_argument('--no-wait', action='store_true', + help='execute command and exit') config.load_app(parser=parser, no_config=True) arg = parser.parse_args() + if arg.no_wait: + no_wait = True + if arg.switch_relay is not None and 'relay' not in arg.modules: raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules') mqtt = MqttWrapper(randomize_client_id=True, client_id='mqtt_node_util') + mqtt.add_connect_callback(on_mqtt_connect) mqtt_node = MqttNode(node_id=arg.node_id, node_secret=nodes_config.get_node(arg.node_id)['password']) @@ -42,6 +73,8 @@ if __name__ == '__main__': # must-have modules ota_module = mqtt_node.load_module('ota') + ota_val = arg.push_ota + mqtt_node.load_module('diagnostics') if arg.modules: @@ -51,18 +84,16 @@ if __name__ == '__main__': 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) + relay_module = module_instance + relay_val = arg.switch_relay try: mqtt.connect_and_loop(loop_forever=False) - - if arg.push_ota: - if not os.path.exists(arg.push_ota): - raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') - ota_module.push_ota(arg.push_ota, 1) - - while True: + while not stop_loop: sleep(0.1) except KeyboardInterrupt: + pass + + finally: mqtt.disconnect() diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index 9ba9443..e5f2c56 100644 --- a/include/py/homekit/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -105,7 +105,7 @@ class MqttNodesConfig(ConfigUnit): 'relay': { 'type': 'dict', 'schema': { - 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid'], 'required': True}, + 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid', 'cooler'], 'required': True}, 'legacy_topics': {'type': 'boolean'} } }, diff --git a/include/py/homekit/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py index 3c2774c..5fc33fe 100644 --- a/include/py/homekit/mqtt/_wrapper.py +++ b/include/py/homekit/mqtt/_wrapper.py @@ -7,6 +7,8 @@ from ..util import strgen class MqttWrapper(Mqtt): _nodes: list[MqttNode] + _connect_callbacks: list[callable] + _disconnect_callbacks: list[callable] def __init__(self, client_id: str, @@ -18,17 +20,30 @@ class MqttWrapper(Mqtt): super().__init__(clean_session=clean_session, client_id=client_id) self._nodes = [] + self._connect_callbacks = [] + self._disconnect_callbacks = [] self._topic_prefix = topic_prefix def on_connect(self, client: mqtt.Client, userdata, flags, rc): super().on_connect(client, userdata, flags, rc) for node in self._nodes: node.on_connect(self) + for f in self._connect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) def on_disconnect(self, client: mqtt.Client, userdata, rc): super().on_disconnect(client, userdata, rc) for node in self._nodes: node.on_disconnect() + for f in self._disconnect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) + def on_message(self, client: mqtt.Client, userdata, msg): try: @@ -40,6 +55,12 @@ class MqttWrapper(Mqtt): except Exception as e: self._logger.exception(str(e)) + def add_connect_callback(self, f: callable): + self._connect_callbacks.append(f) + + def add_disconnect_callback(self, f: callable): + self._disconnect_callbacks.append(f) + def add_node(self, node: MqttNode): self._nodes.append(node) if self._connected: diff --git a/include/py/homekit/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py index e968031..5cbe09b 100644 --- a/include/py/homekit/mqtt/module/relay.py +++ b/include/py/homekit/mqtt/module/relay.py @@ -69,8 +69,7 @@ class MqttRelayModule(MqttModule): mqtt.subscribe_module(self._get_switch_topic(), self) mqtt.subscribe_module('relay/status', self) - def switchpower(self, - enable: bool): + def switchpower(self, enable: bool): payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret, state=enable) self._mqtt_node_ref.publish(self._get_switch_topic(), diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py index a0e7a1f..5b40aae 100644 --- a/include/py/homekit/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -3,6 +3,7 @@ import logging from io import StringIO from collections import OrderedDict +from ..mqtt import MqttNodesConfig _logger = logging.getLogger(__name__) @@ -37,6 +38,8 @@ def platformio_ini(product_config: dict, debug=False, debug_network=False) -> str: node_id = build_specific_defines['CONFIG_NODE_ID'] + if node_id not in MqttNodesConfig().get_nodes().keys(): + raise ValueError(f'node id "{node_id}" is not specified in the config!') # defines defines = { From 54ddea4614dbd31dad577ae5fdb8ec4821490199 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 24 Sep 2023 03:35:51 +0300 Subject: [PATCH 39/51] save --- bin/web_kbn.py | 75 +++++++++++++++--- include/py/homekit/modem/__init__.py | 1 + include/py/homekit/modem/config.py | 5 ++ localwebsite/handlers/ModemHandler.php | 6 -- localwebsite/htdocs/assets/modem.js | 29 ------- .../htdocs/assets => web/kbn_assets}/app.css | 0 .../htdocs/assets => web/kbn_assets}/app.js | 32 +++++++- .../kbn_assets}/bootstrap.min.css | 0 .../kbn_assets}/bootstrap.min.js | 0 .../h265webjs-v20221106-reminified.js | 0 .../h265webjs-dist/h265webjs-v20221106.js | 0 .../missile-120func-v20221120.js | 0 .../missile-120func-v20221120.wasm | Bin .../h265webjs-dist/missile-120func.js | 0 .../h265webjs-dist/missile-256mb-v20221120.js | 0 .../missile-256mb-v20221120.wasm | Bin .../h265webjs-dist/missile-256mb.js | 0 .../h265webjs-dist/missile-512mb-v20221120.js | 0 .../missile-512mb-v20221120.wasm | Bin .../h265webjs-dist/missile-512mb.js | 0 .../h265webjs-dist/missile-format.js | 0 .../h265webjs-dist/missile-v20221120.js | 0 .../h265webjs-dist/missile-v20221120.wasm | Bin .../kbn_assets}/h265webjs-dist/missile.js | 0 .../kbn_assets}/h265webjs-dist/raw-parser.js | 0 .../h265webjs-dist/worker-fetch-dist.js | 0 .../h265webjs-dist/worker-parse-dist.js | 0 .../htdocs/assets => web/kbn_assets}/hls.js | 0 .../assets => web/kbn_assets}/inverter.js | 0 .../assets => web/kbn_assets}/polyfills.js | 0 web/kbn_templates/base.html | 4 +- web/kbn_templates/index.html | 39 +++++++++ 32 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 include/py/homekit/modem/__init__.py create mode 100644 include/py/homekit/modem/config.py delete mode 100644 localwebsite/htdocs/assets/modem.js rename {localwebsite/htdocs/assets => web/kbn_assets}/app.css (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/app.js (94%) rename {localwebsite/htdocs/assets => web/kbn_assets}/bootstrap.min.css (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/bootstrap.min.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/h265webjs-v20221106-reminified.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/h265webjs-v20221106.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-format.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/raw-parser.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/worker-fetch-dist.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/worker-parse-dist.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/hls.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/inverter.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/polyfills.js (100%) create mode 100644 web/kbn_templates/index.html diff --git a/bin/web_kbn.py b/bin/web_kbn.py index b66e2a5..e160fde 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -5,11 +5,13 @@ import aiohttp_jinja2 import os import __py_include +from io import StringIO from typing import Optional from homekit.config import config, AppConfigUnit from homekit.util import homekit_path from aiohttp import web from homekit import http +from homekit.modem import ModemsConfig class WebKbnConfig(AppConfigUnit): @@ -18,10 +20,50 @@ class WebKbnConfig(AppConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'listen_addr': cls._addr_schema(required=True) + 'listen_addr': cls._addr_schema(required=True), + 'assets_public_path': {'type': 'string'} } +STATIC_FILES = [ + 'bootstrap.min.css', + 'bootstrap.min.js', + 'polyfills.js', + 'app.js', + 'app.css' +] + + +def get_js_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'' + + +def get_css_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'' + + +def get_head_static() -> str: + buf = StringIO() + for file in STATIC_FILES: + v = 1 + try: + q_ind = file.index('?') + v = file[q_ind+1:] + file = file[:file.index('?')] + except ValueError: + pass + + if file.endswith('.js'): + buf.write(get_js_link(file, v)) + else: + buf.write(get_css_link(file, v)) + return buf.getvalue() + + class WebSite(http.HTTPServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -31,20 +73,29 @@ class WebSite(http.HTTPServer): loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')) ) + self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) + self.get('/', self.get_index) + self.get('/modems', self.get_modems) - @staticmethod - async def get_index(req: http.Request): - # context = { - # 'username': request.match_info.get("username", ""), - # 'current_date': 'January 27, 2017' - # } - # response = aiohttp_jinja2.render_template("example.html", request, - # context=context) - # return response + async def render_page(self, + req: http.Request, + context: Optional[dict] = None): + if context is None: + context = {} + context = { + **context, + 'head_static': get_head_static(), + 'title': 'this is title' + } + response = aiohttp_jinja2.render_template('index.html', req, context=context) + return response - message = "nothing here, keep lurking" - return http.Response(text=message, content_type='text/plain') + async def get_index(self, req: http.Request): + return await self.render_page(req) + + async def get_modems(self, req: http.Request): + pass if __name__ == '__main__': diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py new file mode 100644 index 0000000..20e75b7 --- /dev/null +++ b/include/py/homekit/modem/__init__.py @@ -0,0 +1 @@ +from .config import ModemsConfig \ No newline at end of file diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py new file mode 100644 index 0000000..039d759 --- /dev/null +++ b/include/py/homekit/modem/config.py @@ -0,0 +1,5 @@ +from ..config import ConfigUnit + + +class ModemsConfig(ConfigUnit): + pass diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index b54b82c..fb91084 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -7,12 +7,6 @@ use libphonenumber\PhoneNumberUtil; class ModemHandler extends RequestHandler { - public function __construct() - { - parent::__construct(); - $this->tpl->add_static('modem.js'); - } - public function GET_status_page() { global $config; diff --git a/localwebsite/htdocs/assets/modem.js b/localwebsite/htdocs/assets/modem.js deleted file mode 100644 index 9fdb91d..0000000 --- a/localwebsite/htdocs/assets/modem.js +++ /dev/null @@ -1,29 +0,0 @@ -var ModemStatus = { - _modems: [], - - init: function(modems) { - for (var i = 0; i < modems.length; i++) { - var modem = modems[i]; - this._modems.push(new ModemStatusUpdater(modem)); - } - } -}; - - -function ModemStatusUpdater(id) { - this.id = id; - this.elem = ge('modem_data_'+id); - this.fetch(); -} -extend(ModemStatusUpdater.prototype, { - fetch: function() { - ajax.get('/modem/get.ajax', { - id: this.id - }).then(({response}) => { - var {html} = response; - this.elem.innerHTML = html; - - // TODO enqueue rerender - }); - }, -}); \ No newline at end of file diff --git a/localwebsite/htdocs/assets/app.css b/web/kbn_assets/app.css similarity index 100% rename from localwebsite/htdocs/assets/app.css rename to web/kbn_assets/app.css diff --git a/localwebsite/htdocs/assets/app.js b/web/kbn_assets/app.js similarity index 94% rename from localwebsite/htdocs/assets/app.js rename to web/kbn_assets/app.js index 37f1307..c187f89 100644 --- a/localwebsite/htdocs/assets/app.js +++ b/web/kbn_assets/app.js @@ -316,4 +316,34 @@ window.Cameras = { return video.canPlayType('application/vnd.apple.mpegurl'); }, }; -})(); \ No newline at end of file +})(); + + +var ModemStatus = { + _modems: [], + + init: function(modems) { + for (var i = 0; i < modems.length; i++) { + var modem = modems[i]; + this._modems.push(new ModemStatusUpdater(modem)); + } + } +}; + +function ModemStatusUpdater(id) { + this.id = id; + this.elem = ge('modem_data_'+id); + this.fetch(); +} +extend(ModemStatusUpdater.prototype, { + fetch: function() { + ajax.get('/modem/get.ajax', { + id: this.id + }).then(({response}) => { + var {html} = response; + this.elem.innerHTML = html; + + // TODO enqueue rerender + }); + }, +}); \ No newline at end of file diff --git a/localwebsite/htdocs/assets/bootstrap.min.css b/web/kbn_assets/bootstrap.min.css similarity index 100% rename from localwebsite/htdocs/assets/bootstrap.min.css rename to web/kbn_assets/bootstrap.min.css diff --git a/localwebsite/htdocs/assets/bootstrap.min.js b/web/kbn_assets/bootstrap.min.js similarity index 100% rename from localwebsite/htdocs/assets/bootstrap.min.js rename to web/kbn_assets/bootstrap.min.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js rename to web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js rename to web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js b/web/kbn_assets/h265webjs-dist/missile-120func.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js rename to web/kbn_assets/h265webjs-dist/missile-120func.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js b/web/kbn_assets/h265webjs-dist/missile-256mb.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js rename to web/kbn_assets/h265webjs-dist/missile-256mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js b/web/kbn_assets/h265webjs-dist/missile-512mb.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js rename to web/kbn_assets/h265webjs-dist/missile-512mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js b/web/kbn_assets/h265webjs-dist/missile-format.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-format.js rename to web/kbn_assets/h265webjs-dist/missile-format.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile.js b/web/kbn_assets/h265webjs-dist/missile.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile.js rename to web/kbn_assets/h265webjs-dist/missile.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js b/web/kbn_assets/h265webjs-dist/raw-parser.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js rename to web/kbn_assets/h265webjs-dist/raw-parser.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js rename to web/kbn_assets/h265webjs-dist/worker-fetch-dist.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js rename to web/kbn_assets/h265webjs-dist/worker-parse-dist.js diff --git a/localwebsite/htdocs/assets/hls.js b/web/kbn_assets/hls.js similarity index 100% rename from localwebsite/htdocs/assets/hls.js rename to web/kbn_assets/hls.js diff --git a/localwebsite/htdocs/assets/inverter.js b/web/kbn_assets/inverter.js similarity index 100% rename from localwebsite/htdocs/assets/inverter.js rename to web/kbn_assets/inverter.js diff --git a/localwebsite/htdocs/assets/polyfills.js b/web/kbn_assets/polyfills.js similarity index 100% rename from localwebsite/htdocs/assets/polyfills.js rename to web/kbn_assets/polyfills.js diff --git a/web/kbn_templates/base.html b/web/kbn_templates/base.html index e567a90..43f7d2a 100644 --- a/web/kbn_templates/base.html +++ b/web/kbn_templates/base.html @@ -9,11 +9,13 @@ window.console && console.error(error); } - {{ head_static }} + {{ head_static | safe }}
+{% block content %} {% endblock %} + {% if js %} {% endif %} diff --git a/web/kbn_templates/index.html b/web/kbn_templates/index.html new file mode 100644 index 0000000..1921b87 --- /dev/null +++ b/web/kbn_templates/index.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
+ + + + + + + + +
Интернет
+ + +
Другое
+ + +
Все камеры (HQ)
+ +
+{% endblock %} \ No newline at end of file From b7cbc2571c1870b4582ead45277d0aa7f961bec8 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 27 Sep 2023 00:54:34 +0300 Subject: [PATCH 40/51] lws: routing updates --- localwebsite/classes/MyOpenWrtUtils.php | 10 ++++++- localwebsite/handlers/InverterHandler.php | 4 ++- localwebsite/handlers/MiscHandler.php | 5 +++- localwebsite/handlers/ModemHandler.php | 27 ++++++++++--------- localwebsite/templates-web/index.twig | 6 ++--- .../templates-web/routing_header.twig | 2 +- 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/localwebsite/classes/MyOpenWrtUtils.php b/localwebsite/classes/MyOpenWrtUtils.php index 6bdfec2..c140fa1 100644 --- a/localwebsite/classes/MyOpenWrtUtils.php +++ b/localwebsite/classes/MyOpenWrtUtils.php @@ -61,6 +61,14 @@ class MyOpenWrtUtils { return $list; } + public static function setUpstream(string $ip) { + return self::run(['homekit-set-default-upstream', $ip]); + } + + public static function getDefaultRoute() { + return self::run(['get-default-route']); + } + // // http functions @@ -128,4 +136,4 @@ class MyOpenWrtUtils { ]; } -} \ No newline at end of file +} diff --git a/localwebsite/handlers/InverterHandler.php b/localwebsite/handlers/InverterHandler.php index 7098e2c..5fa269f 100644 --- a/localwebsite/handlers/InverterHandler.php +++ b/localwebsite/handlers/InverterHandler.php @@ -93,10 +93,12 @@ class InverterHandler extends RequestHandler protected function getClient(): InverterdClient { global $config; + if (isset($_GET['alt']) && $_GET['alt'] == 1) + $config['inverterd_host'] = '192.168.5.223'; $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']); $inv->setFormat('json'); return $inv; } -} \ No newline at end of file +} diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php index 10b4426..4c5a25e 100644 --- a/localwebsite/handlers/MiscHandler.php +++ b/localwebsite/handlers/MiscHandler.php @@ -33,6 +33,9 @@ class MiscHandler extends RequestHandler public function GET_pump_page() { global $config; + if (isset($_GET['alt']) && $_GET['alt'] == 1) + $config['pump_host'] = '192.168.5.223'; + list($set) = $this->input('set'); $client = new GPIORelaydClient($config['pump_host'], $config['pump_port']); @@ -165,4 +168,4 @@ class MiscHandler extends RequestHandler phpinfo(); } -} \ No newline at end of file +} diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index b54b82c..23e4c9a 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -76,7 +76,7 @@ class ModemHandler extends RequestHandler global $config; list($error) = $this->input('error'); - $upstream = self::getCurrentSmallHomeUpstream(); + $upstream = self::getCurrentUpstream(); $current_upstream = [ 'key' => $upstream, @@ -98,12 +98,13 @@ class ModemHandler extends RequestHandler if (!isset($config['modems'][$new_upstream])) redirect('/routing/?error='.urlencode('invalid upstream')); - $current_upstream = self::getCurrentSmallHomeUpstream(); + $current_upstream = self::getCurrentUpstream(); if ($current_upstream != $new_upstream) { - if ($current_upstream != $config['routing_default']) - MyOpenWrtUtils::ipsetDel($current_upstream, $config['routing_smallhome_ip']); - if ($new_upstream != $config['routing_default']) - MyOpenWrtUtils::ipsetAdd($new_upstream, $config['routing_smallhome_ip']); + if ($new_upstream == 'mts-il') + $new_upstream_ip = '192.168.88.1'; + else + $new_upstream_ip = $config['modems'][$new_upstream]['ip']; + MyOpenWrtUtils::setUpstream($new_upstream_ip); } redirect('/routing/'); @@ -264,14 +265,16 @@ class ModemHandler extends RequestHandler } } - protected static function getCurrentSmallHomeUpstream() { + protected static function getCurrentUpstream() { global $config; + $default_route = MyOpenWrtUtils::getDefaultRoute(); + if ($default_route == '192.168.88.1') + $default_route = $config['modems']['mts-il']['ip']; $upstream = null; - $ip_sets = MyOpenWrtUtils::ipsetListAll(); - foreach ($ip_sets as $set => $ips) { - if (in_array($config['routing_smallhome_ip'], $ips)) { - $upstream = $set; + foreach ($config['modems'] as $modem_name => $modem_data) { + if ($default_route == $modem_data['ip']) { + $upstream = $modem_name; break; } } @@ -294,4 +297,4 @@ class ModemHandler extends RequestHandler redirect('/routing/ipsets/?error='.urlencode('invalid ip/network: '.$ip)); } -} \ No newline at end of file +} diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig index bbf6802..b28a078 100644 --- a/localwebsite/templates-web/index.twig +++ b/localwebsite/templates-web/index.twig @@ -20,8 +20,8 @@
Другое
@@ -32,4 +32,4 @@ {% endfor %}
  • Статистика
  • -
    \ No newline at end of file + diff --git a/localwebsite/templates-web/routing_header.twig b/localwebsite/templates-web/routing_header.twig index 8cb5f47..7d07d0a 100644 --- a/localwebsite/templates-web/routing_header.twig +++ b/localwebsite/templates-web/routing_header.twig @@ -5,7 +5,7 @@ } %} {% set routing_tabs = [ - {tab: 'smallhome', url: '/routing/', label: 'Маленький дом'}, + {tab: 'smallhome', url: '/routing/', label: 'Интернет'}, {tab: 'ipsets', url: '/routing/ipsets/', label: 'Правила'}, {tab: 'dhcp', url: '/routing/dhcp/', label: 'DHCP'} ] %} From 69adc549d317217b275a3e0cca689a9b1e7d3263 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 5 Oct 2023 01:35:43 +0300 Subject: [PATCH 41/51] mqtt_node_util: minor help change --- bin/mqtt_node_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py index c1d457c..68d3bd1 100755 --- a/bin/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -50,7 +50,7 @@ if __name__ == '__main__': 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') + help='push OTA, receives path to firmware.bin (not .elf!)') parser.add_argument('--no-wait', action='store_true', help='execute command and exit') From 17b447646752cb141c30684046906961e8d0af9f Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 5 Oct 2023 01:36:24 +0300 Subject: [PATCH 42/51] pio, mqtt: multiple fixes --- include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp | 2 +- include/pio/libs/mqtt/library.json | 2 +- include/pio/libs/temphum/library.json | 2 +- include/py/homekit/mqtt/_config.py | 7 ++++++- include/py/homekit/mqtt/module/ota.py | 2 +- include/py/homekit/pio/products.py | 7 +++++++ 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp index aa769a5..83764ca 100644 --- a/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp +++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp @@ -119,7 +119,7 @@ void Mqtt::reconnect() { void Mqtt::disconnect() { // TODO test how this works??? reconnectTimer.detach(); - client.disconnect(); + client.disconnect(true); } void Mqtt::loop() { diff --git a/include/pio/libs/mqtt/library.json b/include/pio/libs/mqtt/library.json index f3f2504..6238c21 100644 --- a/include/pio/libs/mqtt/library.json +++ b/include/pio/libs/mqtt/library.json @@ -1,6 +1,6 @@ { "name": "homekit_mqtt", - "version": "1.0.11", + "version": "1.0.12", "build": { "flags": "-I../../include" } diff --git a/include/pio/libs/temphum/library.json b/include/pio/libs/temphum/library.json index 329b7ca..4cf5c63 100644 --- a/include/pio/libs/temphum/library.json +++ b/include/pio/libs/temphum/library.json @@ -1,6 +1,6 @@ { "name": "homekit_temphum", - "version": "1.0.3", + "version": "1.0.4", "build": { "flags": "-I../../include" } diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index e5f2c56..4916d8a 100644 --- a/include/py/homekit/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -109,7 +109,12 @@ class MqttNodesConfig(ConfigUnit): 'legacy_topics': {'type': 'boolean'} } }, - 'password': {'type': 'string'} + 'password': {'type': 'string'}, + 'defines': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': {'type': ['string', 'integer']} + } } } } diff --git a/include/py/homekit/mqtt/module/ota.py b/include/py/homekit/mqtt/module/ota.py index cd34332..2f9b216 100644 --- a/include/py/homekit/mqtt/module/ota.py +++ b/include/py/homekit/mqtt/module/ota.py @@ -74,4 +74,4 @@ class MqttOtaModule(MqttModule): if not self._initialized: self._ota_request = (filename, qos) else: - self.do_push_ota(filename, qos) + self.do_push_ota(self._mqtt_node_ref.secret, filename, qos) diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py index 5b40aae..3d5034f 100644 --- a/include/py/homekit/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -41,6 +41,11 @@ def platformio_ini(product_config: dict, if node_id not in MqttNodesConfig().get_nodes().keys(): raise ValueError(f'node id "{node_id}" is not specified in the config!') + try: + node_defines = MqttNodesConfig().get_node(node_id)['defines'] + except KeyError: + node_defines = None + # defines defines = { **product_config['common_defines'], @@ -66,6 +71,8 @@ def platformio_ini(product_config: dict, if build_specific_defines: for k, v in build_specific_defines.items(): defines[k] = v + if node_defines: + defines = {**defines, **node_defines} defines = OrderedDict(sorted(defines.items(), key=lambda t: t[0])) # libs From 05c5d18f7619c28e620d42c0921f81ced780cc2d Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 10 Jan 2024 03:20:10 +0300 Subject: [PATCH 43/51] save --- bin/mqtt_node_util.py | 5 ++- bin/web_kbn.py | 22 +++++++---- include/py/homekit/config/config.py | 19 ++++++++-- include/py/homekit/modem/config.py | 28 +++++++++++++- include/py/homekit/mqtt/_config.py | 20 +++++++++- include/py/homekit/mqtt/module/temphum.py | 41 ++++++++------------ include/py/homekit/util.py | 29 +++++++++----- web/kbn_templates/base.html | 25 ------------ web/kbn_templates/base.j2 | 44 ++++++++++++++++++++++ web/kbn_templates/{index.html => index.j2} | 14 +++---- web/kbn_templates/loading.j2 | 14 +++++++ web/kbn_templates/modems.j2 | 12 ++++++ 12 files changed, 191 insertions(+), 82 deletions(-) delete mode 100644 web/kbn_templates/base.html create mode 100644 web/kbn_templates/base.j2 rename web/kbn_templates/{index.html => index.j2} (63%) create mode 100644 web/kbn_templates/loading.j2 create mode 100644 web/kbn_templates/modems.j2 diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py index c1d457c..5587739 100755 --- a/bin/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -48,7 +48,6 @@ if __name__ == '__main__': 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') parser.add_argument('--no-wait', action='store_true', @@ -80,8 +79,10 @@ if __name__ == '__main__': if arg.modules: for m in arg.modules: kwargs = {} - if m == 'relay' and arg.legacy_relay: + if m == 'relay' and MqttNodesConfig().node_uses_legacy_relay_power_payload(arg.node_id): kwargs['legacy_topics'] = True + if m == 'temphum' and MqttNodesConfig().node_uses_legacy_temphum_data_payload(arg.node_id): + kwargs['legacy_payload'] = True module_instance = mqtt_node.load_module(m, **kwargs) if m == 'relay' and arg.switch_relay is not None: relay_module = module_instance diff --git a/bin/web_kbn.py b/bin/web_kbn.py index e160fde..8b4ca6f 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -75,27 +75,35 @@ class WebSite(http.HTTPServer): self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) - self.get('/', self.get_index) - self.get('/modems', self.get_modems) + self.get('/main.cgi', self.get_index) + self.get('/modems.cgi', self.get_modems) async def render_page(self, req: http.Request, + template_name: str, + title: Optional[str] = None, context: Optional[dict] = None): if context is None: context = {} context = { **context, - 'head_static': get_head_static(), - 'title': 'this is title' + 'head_static': get_head_static() } - response = aiohttp_jinja2.render_template('index.html', req, context=context) + if title is not None: + context['title'] = title + response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context) return response async def get_index(self, req: http.Request): - return await self.render_page(req) + return await self.render_page(req, 'index', + title="Home web site") async def get_modems(self, req: http.Request): - pass + mc = ModemsConfig() + print(mc) + return await self.render_page(req, 'modems', + title='Состояние модемов', + context=dict(modems=ModemsConfig())) if __name__ == '__main__': diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index d424888..abdedad 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -41,6 +41,9 @@ class BaseConfigUnit(ABC): self._data = {} self._logger = logging.getLogger(self.__class__.__name__) + def __iter__(self): + return iter(self._data) + def __getitem__(self, key): return self._data[key] @@ -123,10 +126,10 @@ class ConfigUnit(BaseConfigUnit): return None @classmethod - def _addr_schema(cls, required=False, **kwargs): + def _addr_schema(cls, required=False, only_ip=False, **kwargs): return { 'type': 'addr', - 'coerce': Addr.fromstring, + 'coerce': Addr.fromstring if not only_ip else Addr.fromipstring, 'required': required, **kwargs } @@ -158,6 +161,7 @@ class ConfigUnit(BaseConfigUnit): pass v = MyValidator() + need_document = False if rst == RootSchemaType.DICT: normalized = v.validated({'document': self._data}, @@ -165,16 +169,21 @@ class ConfigUnit(BaseConfigUnit): 'type': 'dict', 'keysrules': {'type': 'string'}, 'valuesrules': schema - }})['document'] + }}) + need_document = True elif rst == RootSchemaType.LIST: v = MyValidator() - normalized = v.validated({'document': self._data}, {'document': schema})['document'] + normalized = v.validated({'document': self._data}, {'document': schema}) + need_document = True else: normalized = v.validated(self._data, schema) if not normalized: raise cerberus.DocumentError(f'validation failed: {v.errors}') + if need_document: + normalized = normalized['document'] + self._data = normalized try: @@ -235,6 +244,8 @@ class TranslationUnit(BaseConfigUnit): class Translation: LANGUAGES = ('en', 'ru') + DEFAULT_LANGUAGE = 'ru' + _langs: dict[str, TranslationUnit] def __init__(self, name: str): diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py index 039d759..16d1ba0 100644 --- a/include/py/homekit/modem/config.py +++ b/include/py/homekit/modem/config.py @@ -1,5 +1,29 @@ -from ..config import ConfigUnit +from ..config import ConfigUnit, Translation +from typing import Optional class ModemsConfig(ConfigUnit): - pass + NAME = 'modems' + + _strings: Translation + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._strings = Translation('modems') + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'type': 'dict', + 'schema': { + 'ip': cls._addr_schema(required=True, only_ip=True), + 'gateway_ip': cls._addr_schema(required=False, only_ip=True), + 'legacy_auth': {'type': 'boolean', 'required': True} + } + } + + def getshortname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['short'] + + def getfullname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['full'] \ No newline at end of file diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index e5f2c56..8aa3bfe 100644 --- a/include/py/homekit/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -92,6 +92,7 @@ class MqttNodesConfig(ConfigUnit): 'type': 'dict', 'schema': { 'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']}, + 'legacy_payload': {'type': 'boolean', 'required': False, 'default': False}, 'interval': {'type': 'integer'}, 'i2c_bus': {'type': 'integer'}, 'tcpserver': { @@ -109,7 +110,12 @@ class MqttNodesConfig(ConfigUnit): 'legacy_topics': {'type': 'boolean'} } }, - 'password': {'type': 'string'} + 'password': {'type': 'string'}, + 'defines': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': {'type': ['string', 'integer']} + } } } } @@ -163,3 +169,15 @@ class MqttNodesConfig(ConfigUnit): else: resdict[name] = node return reslist if only_names else resdict + + def node_uses_legacy_temphum_data_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['temphum']['legacy_payload'] + except KeyError: + return False + + def node_uses_legacy_relay_power_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['relay']['legacy_topics'] + except KeyError: + return False diff --git a/include/py/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py index fd02cca..6deccfe 100644 --- a/include/py/homekit/mqtt/module/temphum.py +++ b/include/py/homekit/mqtt/module/temphum.py @@ -10,8 +10,8 @@ MODULE_NAME = 'MqttTempHumModule' DATA_TOPIC = 'temphum/data' -class MqttTemphumDataPayload(MqttPayload): - FORMAT = '=ddb' +class MqttTemphumLegacyDataPayload(MqttPayload): + FORMAT = '=dd' UNPACKER = { 'temp': two_digits_precision, 'rh': two_digits_precision @@ -19,39 +19,26 @@ class MqttTemphumDataPayload(MqttPayload): temp: float rh: float + + +class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload): + FORMAT = '=ddb' 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): + _legacy_payload: bool + def __init__(self, sensor: Optional[BaseSensor] = None, + legacy_payload=False, write_to_database=False, *args, **kwargs): if sensor is not None: kwargs['tick_interval'] = 10 super().__init__(*args, **kwargs) self._sensor = sensor + self._legacy_payload = legacy_payload def on_connect(self, mqtt: MqttNode): super().on_connect(mqtt) @@ -69,7 +56,7 @@ class MqttTempHumModule(MqttModule): rh = self._sensor.humidity() except: error = 1 - pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error) + pld = self._get_data_payload_cls()(temp=temp, rh=rh, error=error) self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack()) def handle_payload(self, @@ -77,6 +64,10 @@ class MqttTempHumModule(MqttModule): topic: str, payload: bytes) -> Optional[MqttPayload]: if topic == DATA_TOPIC: - message = MqttTemphumDataPayload.unpack(payload) + message = self._get_data_payload_cls().unpack(payload) self._logger.debug(message) return message + + def _get_data_payload_cls(self): + return MqttTemphumLegacyDataPayload if self._legacy_payload else MqttTemphumDataPayload + diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 2680c37..3c73440 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -53,17 +53,21 @@ class Addr: self.host = host self.port = port - @staticmethod - def fromstring(addr: str) -> Addr: - colons = addr.count(':') - if colons != 1: - raise ValueError('invalid host:port format') + @classmethod + def fromstring(cls, addr: str, port_required=True) -> Addr: + if port_required: + colons = addr.count(':') + if colons != 1: + raise ValueError('invalid host:port format') - if not colons: - host = addr - port = None + if not colons: + host = addr + port = None + else: + host, port = addr.split(':') else: - host, port = addr.split(':') + port = None + host = addr validate_ipv4_or_hostname(host, raise_exception=True) @@ -74,12 +78,19 @@ class Addr: return Addr(host, port) + @classmethod + def fromipstring(cls, addr: str) -> Addr: + return cls.fromstring(addr, port_required=False) + def __str__(self): buf = self.host if self.port is not None: buf += ':'+str(self.port) return buf + def __repr__(self): + return self.__str__() + def __iter__(self): yield self.host yield self.port diff --git a/web/kbn_templates/base.html b/web/kbn_templates/base.html deleted file mode 100644 index 43f7d2a..0000000 --- a/web/kbn_templates/base.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - {{ title }} - - - - {{ head_static | safe }} - - -
    - -{% block content %} {% endblock %} - -{% if js %} - -{% endif %} - -
    - - diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 new file mode 100644 index 0000000..d43a08b --- /dev/null +++ b/web/kbn_templates/base.j2 @@ -0,0 +1,44 @@ +{% macro breadcrumbs(history) %} + +{% endmacro %} + + + + + {{ title }} + + + + {{ head_static | safe }} + + +
    + +{% block content %}{% endblock %} + +{% if js %} + +{% endif %} + +
    + + diff --git a/web/kbn_templates/index.html b/web/kbn_templates/index.j2 similarity index 63% rename from web/kbn_templates/index.html rename to web/kbn_templates/index.j2 index 1921b87..e3ab421 100644 --- a/web/kbn_templates/index.html +++ b/web/kbn_templates/index.j2 @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "base.j2" %} {% block content %}
    @@ -16,16 +16,16 @@
    Интернет
    Другое
    Все камеры (HQ)
    diff --git a/web/kbn_templates/loading.j2 b/web/kbn_templates/loading.j2 new file mode 100644 index 0000000..d064a48 --- /dev/null +++ b/web/kbn_templates/loading.j2 @@ -0,0 +1,14 @@ +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 new file mode 100644 index 0000000..f148140 --- /dev/null +++ b/web/kbn_templates/modems.j2 @@ -0,0 +1,12 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'Модемы'}]) }} + +{% for modem in modems %} +
    {{ modems.getfullname(modem) }}
    +
    + {% include "loading.j2" %} +
    +{% endfor %} +{% endblock %} \ No newline at end of file From 57955b596485ecce1ffd4395e23c078358cc5ddd Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Sat, 13 Jan 2024 00:54:32 +0000 Subject: [PATCH 44/51] save something --- bin/ipcam_capture.py | 3 +- include/py/homekit/camera/config.py | 42 ++++++++++++------------ include/py/homekit/camera/types.py | 50 ++++++++++++++++++----------- requirements.txt | 2 +- tasks/df_h.sh | 2 ++ 5 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 tasks/df_h.sh diff --git a/bin/ipcam_capture.py b/bin/ipcam_capture.py index 5de14af..226e12e 100755 --- a/bin/ipcam_capture.py +++ b/bin/ipcam_capture.py @@ -48,7 +48,8 @@ async def run_ffmpeg(cam: int, channel: int): else: debug_args = ['-nostats', '-loglevel', 'error'] - protocol = 'tcp' if ipcam_config.should_use_tcp_for_rtsp(cam) else 'udp' + # protocol = 'tcp' if ipcam_config.should_use_tcp_for_rtsp(cam) else 'udp' + protocol = 'tcp' user, pw = ipcam_config.get_rtsp_creds() ip = ipcam_config.get_camera_ip(cam) path = ipcam_config.get_camera_type(cam).get_channel_url(channel) diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index c7dbc38..8e9bfd5 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -23,17 +23,13 @@ class IpcamConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'cams': { + 'cameras': { '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': { @@ -44,10 +40,18 @@ class IpcamConfig(ConfigUnit): } } }, - 'rtsp_tcp': {'type': 'boolean'} } } }, + 'areas': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': { + 'type': 'list', + 'schema': {'type': ['string', 'integer']} # same type as for 'cameras' keysrules + } + }, + 'camera_ip_template': {'type': 'string', 'required': True}, 'motion_padding': {'type': 'integer', 'required': True}, 'motion_telegram': {'type': 'boolean', 'required': True}, 'fix_interval': {'type': 'integer', 'required': True}, @@ -94,6 +98,7 @@ class IpcamConfig(ConfigUnit): } } + # FIXME def get_all_cam_names(self, filter_by_server: Optional[str] = None, filter_by_disk: Optional[int] = None) -> list[int]: @@ -106,25 +111,22 @@ class IpcamConfig(ConfigUnit): 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_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_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_container(self, camera: int) -> VideoContainerType: + return self.get_camera_type(camera).get_container() - def get_camera_type(self, cam: int) -> CameraType: - return CameraType(self['cams'][cam]['type']) + def get_camera_type(self, camera: int) -> CameraType: + return CameraType(self['cams'][camera]['type']) def get_rtsp_creds(self) -> tuple[str, str]: return self['rtsp_creds']['login'], self['rtsp_creds']['password'] - def 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}' + return self['camera_ip_template'] % (str(camera),) diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index c313b58..da0fcc6 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -1,25 +1,6 @@ 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' @@ -30,6 +11,37 @@ class VideoCodecType(Enum): H265 = 'h265' +class CameraType(Enum): + ESP32 = 'esp32' + ALIEXPRESS_NONAME = 'ali' + HIKVISION_264 = 'hik_264' + HIKVISION_265 = 'hik_265' + + def get_channel_url(self, channel: int) -> str: + if channel not in (1, 2): + raise ValueError(f'channel {channel} is invalid') + if channel == 1: + return '' + elif channel == 2: + if self.value in (CameraType.HIKVISION_264, CameraType.HIKVISION_265): + return '/Streaming/Channels/2' + elif self.value == CameraType.ALIEXPRESS_NONAME: + return '/?stream=1.sdp' + else: + raise ValueError(f'unsupported camera type {self.value}') + + def get_codec(self, channel: int) -> VideoCodecType: + if channel == 1: + return VideoCodecType.H264 if self.value == CameraType.HIKVISION_264 else VideoCodecType.H265 + elif channel == 2: + return VideoCodecType.H265 if self.value == CameraType.ALIEXPRESS_NONAME else VideoCodecType.H264 + else: + raise ValueError(f'unexpected channel {channel}') + + def get_container(self) -> VideoContainerType: + return VideoContainerType.MP4 if self.get_codec(1) == VideoCodecType.H264 else VideoContainerType.MOV + + class TimeFilterType(Enum): FIX = 'fix' MOTION = 'motion' diff --git a/requirements.txt b/requirements.txt index 521ae41..6067436 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ Werkzeug==2.3.6 uwsgi~=2.0.20 python-telegram-bot==20.3 requests==2.31.0 -aiohttp~=3.8.1 +aiohttp~=3.9.1 pytz==2023.3 PyYAML~=6.0 apscheduler==3.10.1 diff --git a/tasks/df_h.sh b/tasks/df_h.sh new file mode 100644 index 0000000..eaa10fe --- /dev/null +++ b/tasks/df_h.sh @@ -0,0 +1,2 @@ +#!/bin/sh +df -h \ No newline at end of file From 7058d0f5063dc9b065248d0a906cf874788caecf Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 13 Sep 2023 09:34:49 +0300 Subject: [PATCH 45/51] save --- bin/mqtt_node_util.py | 56 +++++++-- bin/web_kbn.py | 117 ++++++++++++++++++ include/py/homekit/config/config.py | 21 +++- include/py/homekit/modem/__init__.py | 1 + include/py/homekit/modem/config.py | 29 +++++ include/py/homekit/mqtt/_config.py | 22 +++- include/py/homekit/mqtt/_wrapper.py | 21 ++++ include/py/homekit/mqtt/module/relay.py | 3 +- include/py/homekit/mqtt/module/temphum.py | 41 +++--- include/py/homekit/pio/products.py | 3 + include/py/homekit/util.py | 38 ++++-- localwebsite/handlers/ModemHandler.php | 6 - localwebsite/htdocs/assets/modem.js | 29 ----- requirements.txt | 5 +- .../htdocs/assets => web/kbn_assets}/app.css | 0 .../htdocs/assets => web/kbn_assets}/app.js | 32 ++++- .../kbn_assets}/bootstrap.min.css | 0 .../kbn_assets}/bootstrap.min.js | 0 .../h265webjs-v20221106-reminified.js | 0 .../h265webjs-dist/h265webjs-v20221106.js | 0 .../missile-120func-v20221120.js | 0 .../missile-120func-v20221120.wasm | Bin .../h265webjs-dist/missile-120func.js | 0 .../h265webjs-dist/missile-256mb-v20221120.js | 0 .../missile-256mb-v20221120.wasm | Bin .../h265webjs-dist/missile-256mb.js | 0 .../h265webjs-dist/missile-512mb-v20221120.js | 0 .../missile-512mb-v20221120.wasm | Bin .../h265webjs-dist/missile-512mb.js | 0 .../h265webjs-dist/missile-format.js | 0 .../h265webjs-dist/missile-v20221120.js | 0 .../h265webjs-dist/missile-v20221120.wasm | Bin .../kbn_assets}/h265webjs-dist/missile.js | 0 .../kbn_assets}/h265webjs-dist/raw-parser.js | 0 .../h265webjs-dist/worker-fetch-dist.js | 0 .../h265webjs-dist/worker-parse-dist.js | 0 .../htdocs/assets => web/kbn_assets}/hls.js | 0 .../assets => web/kbn_assets}/inverter.js | 0 .../assets => web/kbn_assets}/polyfills.js | 0 web/kbn_templates/base.j2 | 44 +++++++ web/kbn_templates/index.j2 | 39 ++++++ web/kbn_templates/loading.j2 | 14 +++ web/kbn_templates/modems.j2 | 12 ++ 43 files changed, 439 insertions(+), 94 deletions(-) create mode 100644 bin/web_kbn.py create mode 100644 include/py/homekit/modem/__init__.py create mode 100644 include/py/homekit/modem/config.py delete mode 100644 localwebsite/htdocs/assets/modem.js rename {localwebsite/htdocs/assets => web/kbn_assets}/app.css (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/app.js (94%) rename {localwebsite/htdocs/assets => web/kbn_assets}/bootstrap.min.css (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/bootstrap.min.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/h265webjs-v20221106-reminified.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/h265webjs-v20221106.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-120func.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-256mb.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-512mb.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-format.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-v20221120.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile-v20221120.wasm (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/missile.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/raw-parser.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/worker-fetch-dist.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/h265webjs-dist/worker-parse-dist.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/hls.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/inverter.js (100%) rename {localwebsite/htdocs/assets => web/kbn_assets}/polyfills.js (100%) create mode 100644 web/kbn_templates/base.j2 create mode 100644 web/kbn_templates/index.j2 create mode 100644 web/kbn_templates/loading.j2 create mode 100644 web/kbn_templates/modems.j2 diff --git a/bin/mqtt_node_util.py b/bin/mqtt_node_util.py index cf451fd..5587739 100755 --- a/bin/mqtt_node_util.py +++ b/bin/mqtt_node_util.py @@ -7,12 +7,37 @@ 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 +from homekit.mqtt import MqttNode, MqttWrapper, get_mqtt_modules, MqttNodesConfig +from homekit.mqtt.module.relay import MqttRelayModule +from homekit.mqtt.module.ota import MqttOtaModule mqtt_node: Optional[MqttNode] = None mqtt: Optional[MqttWrapper] = None +relay_module: Optional[MqttOtaModule] = None +relay_val = None + +ota_module: Optional[MqttRelayModule] = None +ota_val = False + +no_wait = False +stop_loop = False + + +def on_mqtt_connect(): + global stop_loop + + if relay_module: + relay_module.switchpower(relay_val == 1) + + if ota_val: + if not os.path.exists(arg.push_ota): + raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') + ota_module.push_ota(arg.push_ota, 1) + + if no_wait: + stop_loop = True + if __name__ == '__main__': nodes_config = MqttNodesConfig() @@ -23,18 +48,23 @@ if __name__ == '__main__': 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') + parser.add_argument('--no-wait', action='store_true', + help='execute command and exit') config.load_app(parser=parser, no_config=True) arg = parser.parse_args() + if arg.no_wait: + no_wait = True + if arg.switch_relay is not None and 'relay' not in arg.modules: raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules') mqtt = MqttWrapper(randomize_client_id=True, client_id='mqtt_node_util') + mqtt.add_connect_callback(on_mqtt_connect) mqtt_node = MqttNode(node_id=arg.node_id, node_secret=nodes_config.get_node(arg.node_id)['password']) @@ -42,27 +72,29 @@ if __name__ == '__main__': # must-have modules ota_module = mqtt_node.load_module('ota') + ota_val = arg.push_ota + mqtt_node.load_module('diagnostics') if arg.modules: for m in arg.modules: kwargs = {} - if m == 'relay' and arg.legacy_relay: + if m == 'relay' and MqttNodesConfig().node_uses_legacy_relay_power_payload(arg.node_id): kwargs['legacy_topics'] = True + if m == 'temphum' and MqttNodesConfig().node_uses_legacy_temphum_data_payload(arg.node_id): + kwargs['legacy_payload'] = True module_instance = mqtt_node.load_module(m, **kwargs) if m == 'relay' and arg.switch_relay is not None: - module_instance.switchpower(arg.switch_relay == 1) + relay_module = module_instance + relay_val = arg.switch_relay try: mqtt.connect_and_loop(loop_forever=False) - - if arg.push_ota: - if not os.path.exists(arg.push_ota): - raise OSError(f'--push-ota: file \"{arg.push_ota}\" does not exists') - ota_module.push_ota(arg.push_ota, 1) - - while True: + while not stop_loop: sleep(0.1) except KeyboardInterrupt: + pass + + finally: mqtt.disconnect() diff --git a/bin/web_kbn.py b/bin/web_kbn.py new file mode 100644 index 0000000..8b4ca6f --- /dev/null +++ b/bin/web_kbn.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import asyncio +import jinja2 +import aiohttp_jinja2 +import os +import __py_include + +from io import StringIO +from typing import Optional +from homekit.config import config, AppConfigUnit +from homekit.util import homekit_path +from aiohttp import web +from homekit import http +from homekit.modem import ModemsConfig + + +class WebKbnConfig(AppConfigUnit): + NAME = 'web_kbn' + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'listen_addr': cls._addr_schema(required=True), + 'assets_public_path': {'type': 'string'} + } + + +STATIC_FILES = [ + 'bootstrap.min.css', + 'bootstrap.min.js', + 'polyfills.js', + 'app.js', + 'app.css' +] + + +def get_js_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'' + + +def get_css_link(file, version) -> str: + if version: + file += f'?version={version}' + return f'' + + +def get_head_static() -> str: + buf = StringIO() + for file in STATIC_FILES: + v = 1 + try: + q_ind = file.index('?') + v = file[q_ind+1:] + file = file[:file.index('?')] + except ValueError: + pass + + if file.endswith('.js'): + buf.write(get_js_link(file, v)) + else: + buf.write(get_css_link(file, v)) + return buf.getvalue() + + +class WebSite(http.HTTPServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + aiohttp_jinja2.setup( + self.app, + loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')) + ) + + self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) + + self.get('/main.cgi', self.get_index) + self.get('/modems.cgi', self.get_modems) + + async def render_page(self, + req: http.Request, + template_name: str, + title: Optional[str] = None, + context: Optional[dict] = None): + if context is None: + context = {} + context = { + **context, + 'head_static': get_head_static() + } + if title is not None: + context['title'] = title + response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context) + return response + + async def get_index(self, req: http.Request): + return await self.render_page(req, 'index', + title="Home web site") + + async def get_modems(self, req: http.Request): + mc = ModemsConfig() + print(mc) + return await self.render_page(req, 'modems', + title='Состояние модемов', + context=dict(modems=ModemsConfig())) + + +if __name__ == '__main__': + config.load_app(WebKbnConfig) + + loop = asyncio.get_event_loop() + # print(config.app_config) + + print(config.app_config['listen_addr'].host) + server = WebSite(config.app_config['listen_addr']) + server.run() diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 5fe1ae8..abdedad 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -41,6 +41,9 @@ class BaseConfigUnit(ABC): self._data = {} self._logger = logging.getLogger(self.__class__.__name__) + def __iter__(self): + return iter(self._data) + def __getitem__(self, key): return self._data[key] @@ -123,10 +126,10 @@ class ConfigUnit(BaseConfigUnit): return None @classmethod - def _addr_schema(cls, required=False, **kwargs): + def _addr_schema(cls, required=False, only_ip=False, **kwargs): return { 'type': 'addr', - 'coerce': Addr.fromstring, + 'coerce': Addr.fromstring if not only_ip else Addr.fromipstring, 'required': required, **kwargs } @@ -158,6 +161,7 @@ class ConfigUnit(BaseConfigUnit): pass v = MyValidator() + need_document = False if rst == RootSchemaType.DICT: normalized = v.validated({'document': self._data}, @@ -165,16 +169,21 @@ class ConfigUnit(BaseConfigUnit): 'type': 'dict', 'keysrules': {'type': 'string'}, 'valuesrules': schema - }})['document'] + }}) + need_document = True elif rst == RootSchemaType.LIST: v = MyValidator() - normalized = v.validated({'document': self._data}, {'document': schema})['document'] + normalized = v.validated({'document': self._data}, {'document': schema}) + need_document = True else: normalized = v.validated(self._data, schema) if not normalized: raise cerberus.DocumentError(f'validation failed: {v.errors}') + if need_document: + normalized = normalized['document'] + self._data = normalized try: @@ -235,6 +244,8 @@ class TranslationUnit(BaseConfigUnit): class Translation: LANGUAGES = ('en', 'ru') + DEFAULT_LANGUAGE = 'ru' + _langs: dict[str, TranslationUnit] def __init__(self, name: str): @@ -278,9 +289,7 @@ class Config: 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 diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py new file mode 100644 index 0000000..20e75b7 --- /dev/null +++ b/include/py/homekit/modem/__init__.py @@ -0,0 +1 @@ +from .config import ModemsConfig \ No newline at end of file diff --git a/include/py/homekit/modem/config.py b/include/py/homekit/modem/config.py new file mode 100644 index 0000000..16d1ba0 --- /dev/null +++ b/include/py/homekit/modem/config.py @@ -0,0 +1,29 @@ +from ..config import ConfigUnit, Translation +from typing import Optional + + +class ModemsConfig(ConfigUnit): + NAME = 'modems' + + _strings: Translation + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._strings = Translation('modems') + + @classmethod + def schema(cls) -> Optional[dict]: + return { + 'type': 'dict', + 'schema': { + 'ip': cls._addr_schema(required=True, only_ip=True), + 'gateway_ip': cls._addr_schema(required=False, only_ip=True), + 'legacy_auth': {'type': 'boolean', 'required': True} + } + } + + def getshortname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['short'] + + def getfullname(self, modem: str, lang=Translation.DEFAULT_LANGUAGE): + return self._strings.get(lang)[modem]['full'] \ No newline at end of file diff --git a/include/py/homekit/mqtt/_config.py b/include/py/homekit/mqtt/_config.py index 9ba9443..8aa3bfe 100644 --- a/include/py/homekit/mqtt/_config.py +++ b/include/py/homekit/mqtt/_config.py @@ -92,6 +92,7 @@ class MqttNodesConfig(ConfigUnit): 'type': 'dict', 'schema': { 'module': {'type': 'string', 'required': True, 'allowed': ['si7021', 'dht12']}, + 'legacy_payload': {'type': 'boolean', 'required': False, 'default': False}, 'interval': {'type': 'integer'}, 'i2c_bus': {'type': 'integer'}, 'tcpserver': { @@ -105,11 +106,16 @@ class MqttNodesConfig(ConfigUnit): 'relay': { 'type': 'dict', 'schema': { - 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid'], 'required': True}, + 'device_type': {'type': 'string', 'allowed': ['lamp', 'pump', 'solenoid', 'cooler'], 'required': True}, 'legacy_topics': {'type': 'boolean'} } }, - 'password': {'type': 'string'} + 'password': {'type': 'string'}, + 'defines': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': {'type': ['string', 'integer']} + } } } } @@ -163,3 +169,15 @@ class MqttNodesConfig(ConfigUnit): else: resdict[name] = node return reslist if only_names else resdict + + def node_uses_legacy_temphum_data_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['temphum']['legacy_payload'] + except KeyError: + return False + + def node_uses_legacy_relay_power_payload(self, node_id: str) -> bool: + try: + return self.get_node(node_id)['relay']['legacy_topics'] + except KeyError: + return False diff --git a/include/py/homekit/mqtt/_wrapper.py b/include/py/homekit/mqtt/_wrapper.py index 3c2774c..5fc33fe 100644 --- a/include/py/homekit/mqtt/_wrapper.py +++ b/include/py/homekit/mqtt/_wrapper.py @@ -7,6 +7,8 @@ from ..util import strgen class MqttWrapper(Mqtt): _nodes: list[MqttNode] + _connect_callbacks: list[callable] + _disconnect_callbacks: list[callable] def __init__(self, client_id: str, @@ -18,17 +20,30 @@ class MqttWrapper(Mqtt): super().__init__(clean_session=clean_session, client_id=client_id) self._nodes = [] + self._connect_callbacks = [] + self._disconnect_callbacks = [] self._topic_prefix = topic_prefix def on_connect(self, client: mqtt.Client, userdata, flags, rc): super().on_connect(client, userdata, flags, rc) for node in self._nodes: node.on_connect(self) + for f in self._connect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) def on_disconnect(self, client: mqtt.Client, userdata, rc): super().on_disconnect(client, userdata, rc) for node in self._nodes: node.on_disconnect() + for f in self._disconnect_callbacks: + try: + f() + except Exception as e: + self._logger.exception(e) + def on_message(self, client: mqtt.Client, userdata, msg): try: @@ -40,6 +55,12 @@ class MqttWrapper(Mqtt): except Exception as e: self._logger.exception(str(e)) + def add_connect_callback(self, f: callable): + self._connect_callbacks.append(f) + + def add_disconnect_callback(self, f: callable): + self._disconnect_callbacks.append(f) + def add_node(self, node: MqttNode): self._nodes.append(node) if self._connected: diff --git a/include/py/homekit/mqtt/module/relay.py b/include/py/homekit/mqtt/module/relay.py index e968031..5cbe09b 100644 --- a/include/py/homekit/mqtt/module/relay.py +++ b/include/py/homekit/mqtt/module/relay.py @@ -69,8 +69,7 @@ class MqttRelayModule(MqttModule): mqtt.subscribe_module(self._get_switch_topic(), self) mqtt.subscribe_module('relay/status', self) - def switchpower(self, - enable: bool): + def switchpower(self, enable: bool): payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret, state=enable) self._mqtt_node_ref.publish(self._get_switch_topic(), diff --git a/include/py/homekit/mqtt/module/temphum.py b/include/py/homekit/mqtt/module/temphum.py index fd02cca..6deccfe 100644 --- a/include/py/homekit/mqtt/module/temphum.py +++ b/include/py/homekit/mqtt/module/temphum.py @@ -10,8 +10,8 @@ MODULE_NAME = 'MqttTempHumModule' DATA_TOPIC = 'temphum/data' -class MqttTemphumDataPayload(MqttPayload): - FORMAT = '=ddb' +class MqttTemphumLegacyDataPayload(MqttPayload): + FORMAT = '=dd' UNPACKER = { 'temp': two_digits_precision, 'rh': two_digits_precision @@ -19,39 +19,26 @@ class MqttTemphumDataPayload(MqttPayload): temp: float rh: float + + +class MqttTemphumDataPayload(MqttTemphumLegacyDataPayload): + FORMAT = '=ddb' 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): + _legacy_payload: bool + def __init__(self, sensor: Optional[BaseSensor] = None, + legacy_payload=False, write_to_database=False, *args, **kwargs): if sensor is not None: kwargs['tick_interval'] = 10 super().__init__(*args, **kwargs) self._sensor = sensor + self._legacy_payload = legacy_payload def on_connect(self, mqtt: MqttNode): super().on_connect(mqtt) @@ -69,7 +56,7 @@ class MqttTempHumModule(MqttModule): rh = self._sensor.humidity() except: error = 1 - pld = MqttTemphumDataPayload(temp=temp, rh=rh, error=error) + pld = self._get_data_payload_cls()(temp=temp, rh=rh, error=error) self._mqtt_node_ref.publish(DATA_TOPIC, pld.pack()) def handle_payload(self, @@ -77,6 +64,10 @@ class MqttTempHumModule(MqttModule): topic: str, payload: bytes) -> Optional[MqttPayload]: if topic == DATA_TOPIC: - message = MqttTemphumDataPayload.unpack(payload) + message = self._get_data_payload_cls().unpack(payload) self._logger.debug(message) return message + + def _get_data_payload_cls(self): + return MqttTemphumLegacyDataPayload if self._legacy_payload else MqttTemphumDataPayload + diff --git a/include/py/homekit/pio/products.py b/include/py/homekit/pio/products.py index a0e7a1f..5b40aae 100644 --- a/include/py/homekit/pio/products.py +++ b/include/py/homekit/pio/products.py @@ -3,6 +3,7 @@ import logging from io import StringIO from collections import OrderedDict +from ..mqtt import MqttNodesConfig _logger = logging.getLogger(__name__) @@ -37,6 +38,8 @@ def platformio_ini(product_config: dict, debug=False, debug_network=False) -> str: node_id = build_specific_defines['CONFIG_NODE_ID'] + if node_id not in MqttNodesConfig().get_nodes().keys(): + raise ValueError(f'node id "{node_id}" is not specified in the config!') # defines defines = { diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 22bba86..3c73440 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -9,6 +9,7 @@ import logging import string import random import re +import os from enum import Enum from datetime import datetime @@ -52,17 +53,21 @@ class Addr: self.host = host self.port = port - @staticmethod - def fromstring(addr: str) -> Addr: - colons = addr.count(':') - if colons != 1: - raise ValueError('invalid host:port format') + @classmethod + def fromstring(cls, addr: str, port_required=True) -> Addr: + if port_required: + colons = addr.count(':') + if colons != 1: + raise ValueError('invalid host:port format') - if not colons: - host = addr - port = None + if not colons: + host = addr + port = None + else: + host, port = addr.split(':') else: - host, port = addr.split(':') + port = None + host = addr validate_ipv4_or_hostname(host, raise_exception=True) @@ -73,12 +78,19 @@ class Addr: return Addr(host, port) + @classmethod + def fromipstring(cls, addr: str) -> Addr: + return cls.fromstring(addr, port_required=False) + def __str__(self): buf = self.host if self.port is not None: buf += ':'+str(self.port) return buf + def __repr__(self): + return self.__str__() + def __iter__(self): yield self.host yield self.port @@ -252,4 +264,10 @@ def next_tick_gen(freq): t = time.time() while True: t += freq - yield max(t - time.time(), 0) \ No newline at end of file + yield max(t - time.time(), 0) + + +def homekit_path(*args) -> str: + return os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', *args) + ) diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index 23e4c9a..6743fe9 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -7,12 +7,6 @@ use libphonenumber\PhoneNumberUtil; class ModemHandler extends RequestHandler { - public function __construct() - { - parent::__construct(); - $this->tpl->add_static('modem.js'); - } - public function GET_status_page() { global $config; diff --git a/localwebsite/htdocs/assets/modem.js b/localwebsite/htdocs/assets/modem.js deleted file mode 100644 index 9fdb91d..0000000 --- a/localwebsite/htdocs/assets/modem.js +++ /dev/null @@ -1,29 +0,0 @@ -var ModemStatus = { - _modems: [], - - init: function(modems) { - for (var i = 0; i < modems.length; i++) { - var modem = modems[i]; - this._modems.push(new ModemStatusUpdater(modem)); - } - } -}; - - -function ModemStatusUpdater(id) { - this.id = id; - this.elem = ge('modem_data_'+id); - this.fetch(); -} -extend(ModemStatusUpdater.prototype, { - fetch: function() { - ajax.get('/modem/get.ajax', { - id: this.id - }).then(({response}) => { - var {html} = response; - this.elem.innerHTML = html; - - // TODO enqueue rerender - }); - }, -}); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6067436..8fa67c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,7 @@ cerberus~=1.3.4 # following can be installed from debian repositories # matplotlib~=3.5.0 -Pillow==9.5.0 \ No newline at end of file +Pillow==9.5.0 + +jinja2~=3.1.2 +aiohttp-jinja2~=1.5.1 \ No newline at end of file diff --git a/localwebsite/htdocs/assets/app.css b/web/kbn_assets/app.css similarity index 100% rename from localwebsite/htdocs/assets/app.css rename to web/kbn_assets/app.css diff --git a/localwebsite/htdocs/assets/app.js b/web/kbn_assets/app.js similarity index 94% rename from localwebsite/htdocs/assets/app.js rename to web/kbn_assets/app.js index 37f1307..c187f89 100644 --- a/localwebsite/htdocs/assets/app.js +++ b/web/kbn_assets/app.js @@ -316,4 +316,34 @@ window.Cameras = { return video.canPlayType('application/vnd.apple.mpegurl'); }, }; -})(); \ No newline at end of file +})(); + + +var ModemStatus = { + _modems: [], + + init: function(modems) { + for (var i = 0; i < modems.length; i++) { + var modem = modems[i]; + this._modems.push(new ModemStatusUpdater(modem)); + } + } +}; + +function ModemStatusUpdater(id) { + this.id = id; + this.elem = ge('modem_data_'+id); + this.fetch(); +} +extend(ModemStatusUpdater.prototype, { + fetch: function() { + ajax.get('/modem/get.ajax', { + id: this.id + }).then(({response}) => { + var {html} = response; + this.elem.innerHTML = html; + + // TODO enqueue rerender + }); + }, +}); \ No newline at end of file diff --git a/localwebsite/htdocs/assets/bootstrap.min.css b/web/kbn_assets/bootstrap.min.css similarity index 100% rename from localwebsite/htdocs/assets/bootstrap.min.css rename to web/kbn_assets/bootstrap.min.css diff --git a/localwebsite/htdocs/assets/bootstrap.min.js b/web/kbn_assets/bootstrap.min.js similarity index 100% rename from localwebsite/htdocs/assets/bootstrap.min.js rename to web/kbn_assets/bootstrap.min.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106-reminified.js rename to web/kbn_assets/h265webjs-dist/h265webjs-v20221106-reminified.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js b/web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/h265webjs-v20221106.js rename to web/kbn_assets/h265webjs-dist/h265webjs-v20221106.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-120func-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-120func-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js b/web/kbn_assets/h265webjs-dist/missile-120func.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-120func.js rename to web/kbn_assets/h265webjs-dist/missile-120func.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-256mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js b/web/kbn_assets/h265webjs-dist/missile-256mb.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-256mb.js rename to web/kbn_assets/h265webjs-dist/missile-256mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-512mb-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js b/web/kbn_assets/h265webjs-dist/missile-512mb.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-512mb.js rename to web/kbn_assets/h265webjs-dist/missile-512mb.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-format.js b/web/kbn_assets/h265webjs-dist/missile-format.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-format.js rename to web/kbn_assets/h265webjs-dist/missile-format.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js b/web/kbn_assets/h265webjs-dist/missile-v20221120.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.js rename to web/kbn_assets/h265webjs-dist/missile-v20221120.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm b/web/kbn_assets/h265webjs-dist/missile-v20221120.wasm similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile-v20221120.wasm rename to web/kbn_assets/h265webjs-dist/missile-v20221120.wasm diff --git a/localwebsite/htdocs/assets/h265webjs-dist/missile.js b/web/kbn_assets/h265webjs-dist/missile.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/missile.js rename to web/kbn_assets/h265webjs-dist/missile.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js b/web/kbn_assets/h265webjs-dist/raw-parser.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/raw-parser.js rename to web/kbn_assets/h265webjs-dist/raw-parser.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js b/web/kbn_assets/h265webjs-dist/worker-fetch-dist.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/worker-fetch-dist.js rename to web/kbn_assets/h265webjs-dist/worker-fetch-dist.js diff --git a/localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js b/web/kbn_assets/h265webjs-dist/worker-parse-dist.js similarity index 100% rename from localwebsite/htdocs/assets/h265webjs-dist/worker-parse-dist.js rename to web/kbn_assets/h265webjs-dist/worker-parse-dist.js diff --git a/localwebsite/htdocs/assets/hls.js b/web/kbn_assets/hls.js similarity index 100% rename from localwebsite/htdocs/assets/hls.js rename to web/kbn_assets/hls.js diff --git a/localwebsite/htdocs/assets/inverter.js b/web/kbn_assets/inverter.js similarity index 100% rename from localwebsite/htdocs/assets/inverter.js rename to web/kbn_assets/inverter.js diff --git a/localwebsite/htdocs/assets/polyfills.js b/web/kbn_assets/polyfills.js similarity index 100% rename from localwebsite/htdocs/assets/polyfills.js rename to web/kbn_assets/polyfills.js diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 new file mode 100644 index 0000000..d43a08b --- /dev/null +++ b/web/kbn_templates/base.j2 @@ -0,0 +1,44 @@ +{% macro breadcrumbs(history) %} + +{% endmacro %} + + + + + {{ title }} + + + + {{ head_static | safe }} + + +
    + +{% block content %}{% endblock %} + +{% if js %} + +{% endif %} + +
    + + diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2 new file mode 100644 index 0000000..e3ab421 --- /dev/null +++ b/web/kbn_templates/index.j2 @@ -0,0 +1,39 @@ +{% extends "base.j2" %} + +{% block content %} +
    + + + + + + + + +
    Интернет
    + + +
    Другое
    + + +
    Все камеры (HQ)
    + +
    +{% endblock %} \ No newline at end of file diff --git a/web/kbn_templates/loading.j2 b/web/kbn_templates/loading.j2 new file mode 100644 index 0000000..d064a48 --- /dev/null +++ b/web/kbn_templates/loading.j2 @@ -0,0 +1,14 @@ +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 new file mode 100644 index 0000000..f148140 --- /dev/null +++ b/web/kbn_templates/modems.j2 @@ -0,0 +1,12 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'Модемы'}]) }} + +{% for modem in modems %} +
    {{ modems.getfullname(modem) }}
    +
    + {% include "loading.j2" %} +
    +{% endfor %} +{% endblock %} \ No newline at end of file From da5db8bc280deab0e2081f39d2f32aabb2372afe Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Tue, 16 Jan 2024 02:05:00 +0300 Subject: [PATCH 46/51] wip --- bin/web_kbn.py | 90 ++++- include/py/homekit/config/config.py | 3 + include/py/homekit/http/__init__.py | 4 +- include/py/homekit/http/http.py | 6 + include/py/homekit/modem/__init__.py | 3 +- include/py/homekit/modem/e3372.py | 253 ++++++++++++++ include/py/homekit/util.py | 21 +- localwebsite/classes/E3372.php | 310 ------------------ localwebsite/handlers/ModemHandler.php | 85 ----- localwebsite/templates-web/modem_data.twig | 14 - .../templates-web/modem_status_page.twig | 19 -- .../templates-web/modem_verbose_page.twig | 15 - localwebsite/templates-web/spinner.twig | 14 - test/test_modems.py | 9 + web/kbn_assets/app.css | 2 +- web/kbn_assets/app.js | 38 ++- web/kbn_templates/base.j2 | 6 +- web/kbn_templates/modem_data.j2 | 13 + web/kbn_templates/modem_verbose.j2 | 18 + web/kbn_templates/modems.j2 | 4 + .../kbn_templates/signal_level.j2 | 2 +- 21 files changed, 433 insertions(+), 496 deletions(-) create mode 100644 include/py/homekit/modem/e3372.py delete mode 100644 localwebsite/classes/E3372.php delete mode 100644 localwebsite/templates-web/modem_data.twig delete mode 100644 localwebsite/templates-web/modem_status_page.twig delete mode 100644 localwebsite/templates-web/modem_verbose_page.twig delete mode 100644 localwebsite/templates-web/spinner.twig create mode 100755 test/test_modems.py create mode 100644 web/kbn_templates/modem_data.j2 create mode 100644 web/kbn_templates/modem_verbose.j2 rename localwebsite/templates-web/signal_level.twig => web/kbn_templates/signal_level.j2 (80%) diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 8b4ca6f..75437f1 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -2,16 +2,18 @@ import asyncio import jinja2 import aiohttp_jinja2 +import json import os +import re import __py_include from io import StringIO -from typing import Optional +from typing import Optional, Union from homekit.config import config, AppConfigUnit -from homekit.util import homekit_path +from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string from aiohttp import web from homekit import http -from homekit.modem import ModemsConfig +from homekit.modem import ModemsConfig, E3372, MacroNetWorkType class WebKbnConfig(AppConfigUnit): @@ -49,7 +51,7 @@ def get_css_link(file, version) -> str: def get_head_static() -> str: buf = StringIO() for file in STATIC_FILES: - v = 1 + v = 2 try: q_ind = file.index('?') v = file[q_ind+1:] @@ -64,19 +66,52 @@ def get_head_static() -> str: return buf.getvalue() +def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: + cl = E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth']) + + signal = cl.device_signal + status = cl.monitoring_status + traffic = cl.traffic_stats + + if get_raw: + device_info = cl.device_information + dialup_conn = cl.dialup_connection + return signal, status, traffic, device_info, dialup_conn + else: + network_type_label = re.sub('^MACRO_NET_WORK_TYPE(_EX)?_', '', MacroNetWorkType(int(status['CurrentNetworkType'])).name) + return { + 'type': network_type_label, + 'level': int(status['SignalIcon']) if 'SignalIcon' in status else 0, + 'rssi': signal['rssi'], + 'sinr': signal['sinr'], + 'connected_time': seconds_to_human_readable_string(int(traffic['CurrentConnectTime'])), + 'downloaded': filesize_fmt(int(traffic['CurrentDownload'])), + 'uploaded': filesize_fmt(int(traffic['CurrentUpload'])) + } + + class WebSite(http.HTTPServer): + _modems_config: ModemsConfig + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._modems_config = ModemsConfig() + aiohttp_jinja2.setup( self.app, - loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')) + loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')), + autoescape=jinja2.select_autoescape(['html', 'xml']), ) + env = aiohttp_jinja2.get_env(self.app) + env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':')) self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) self.get('/main.cgi', self.get_index) self.get('/modems.cgi', self.get_modems) + self.get('/modems/info.ajx', self.get_modems_ajax) + self.get('/modems/verbose.cgi', self.get_modems_verbose) async def render_page(self, req: http.Request, @@ -99,19 +134,50 @@ class WebSite(http.HTTPServer): title="Home web site") async def get_modems(self, req: http.Request): - mc = ModemsConfig() - print(mc) return await self.render_page(req, 'modems', title='Состояние модемов', - context=dict(modems=ModemsConfig())) + context=dict(modems=self._modems_config)) + + async def get_modems_ajax(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.getkeys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg)) + + html = aiohttp_jinja2.render_string('modem_data.j2', req, context=dict( + modem_data=modem_data, + modem=modem + )) + + return self.ok({'html': html}) + + async def get_modems_verbose(self, req: http.Request): + modem = req.query.get('id', None) + if modem not in self._modems_config.getkeys(): + raise ValueError('invalid modem id') + + modem_cfg = self._modems_config.get(modem) + loop = asyncio.get_event_loop() + signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True)) + data = [ + ['Signal', signal], + ['Connection', status], + ['Traffic', traffic], + ['Device info', device], + ['Dialup connection', dialup_conn] + ] + + modem_name = self._modems_config.getfullname(modem) + return await self.render_page(req, 'modem_verbose', + title=f'Подробная информация о модеме "{modem_name}"', + context=dict(data=data, modem_name=modem_name)) if __name__ == '__main__': config.load_app(WebKbnConfig) - loop = asyncio.get_event_loop() - # print(config.app_config) - - print(config.app_config['listen_addr'].host) server = WebSite(config.app_config['listen_addr']) server.run() diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index abdedad..eb2ad82 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -78,6 +78,9 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') + def getkeys(self): + return list(self._data.keys()) + class ConfigUnit(BaseConfigUnit): NAME = 'dumb' diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py index 6030e95..d019e4c 100644 --- a/include/py/homekit/http/__init__.py +++ b/include/py/homekit/http/__init__.py @@ -1,2 +1,2 @@ -from .http import serve, ok, routes, HTTPServer -from aiohttp.web import FileResponse, StreamResponse, Request, Response +from .http import serve, ok, routes, HTTPServer, HTTPMethod +from aiohttp.web import FileResponse, StreamResponse, Request, Response \ No newline at end of file diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py index 3e70751..9b76d9a 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,6 +1,7 @@ import logging import asyncio +from enum import Enum from aiohttp import web from aiohttp.web import Response from aiohttp.web_exceptions import HTTPNotFound @@ -104,3 +105,8 @@ class HTTPServer: def plain(self, text: str): return Response(text=text, content_type='text/plain') + + +class HTTPMethod(Enum): + GET = 'GET' + POST = 'POST' diff --git a/include/py/homekit/modem/__init__.py b/include/py/homekit/modem/__init__.py index 20e75b7..ea0930e 100644 --- a/include/py/homekit/modem/__init__.py +++ b/include/py/homekit/modem/__init__.py @@ -1 +1,2 @@ -from .config import ModemsConfig \ No newline at end of file +from .config import ModemsConfig +from .e3372 import E3372, MacroNetWorkType diff --git a/include/py/homekit/modem/e3372.py b/include/py/homekit/modem/e3372.py new file mode 100644 index 0000000..f68db5a --- /dev/null +++ b/include/py/homekit/modem/e3372.py @@ -0,0 +1,253 @@ +import requests +import xml.etree.ElementTree as ElementTree + +from ..util import Addr +from enum import Enum +from ..http import HTTPMethod +from typing import Union + + +class Error(Enum): + ERROR_SYSTEM_NO_SUPPORT = 100002 + ERROR_SYSTEM_NO_RIGHTS = 100003 + ERROR_SYSTEM_BUSY = 100004 + ERROR_LOGIN_USERNAME_WRONG = 108001 + ERROR_LOGIN_PASSWORD_WRONG = 108002 + ERROR_LOGIN_ALREADY_LOGIN = 108003 + ERROR_LOGIN_USERNAME_PWD_WRONG = 108006 + ERROR_LOGIN_USERNAME_PWD_ORERRUN = 108007 + ERROR_LOGIN_TOUCH_ALREADY_LOGIN = 108009 + ERROR_VOICE_BUSY = 120001 + ERROR_WRONG_TOKEN = 125001 + ERROR_WRONG_SESSION = 125002 + ERROR_WRONG_SESSION_TOKEN = 125003 + + +class WifiStatus(Enum): + WIFI_CONNECTING = '900' + WIFI_CONNECTED = '901' + WIFI_DISCONNECTED = '902' + WIFI_DISCONNECTING = '903' + + +class Cradle(Enum): + CRADLE_CONNECTING = '900' + CRADLE_CONNECTED = '901' + CRADLE_DISCONNECTED = '902' + CRADLE_DISCONNECTING = '903' + CRADLE_CONNECTFAILED = '904' + CRADLE_CONNECTSTATUSNULL = '905' + CRANDLE_CONNECTSTATUSERRO = '906' + + +class MacroEVDOLevel(Enum): + MACRO_EVDO_LEVEL_ZERO = '0' + MACRO_EVDO_LEVEL_ONE = '1' + MACRO_EVDO_LEVEL_TWO = '2' + MACRO_EVDO_LEVEL_THREE = '3' + MACRO_EVDO_LEVEL_FOUR = '4' + MACRO_EVDO_LEVEL_FIVE = '5' + + +class MacroNetWorkType(Enum): + MACRO_NET_WORK_TYPE_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_GSM = 1 + MACRO_NET_WORK_TYPE_GPRS = 2 + MACRO_NET_WORK_TYPE_EDGE = 3 + MACRO_NET_WORK_TYPE_WCDMA = 4 + MACRO_NET_WORK_TYPE_HSDPA = 5 + MACRO_NET_WORK_TYPE_HSUPA = 6 + MACRO_NET_WORK_TYPE_HSPA = 7 + MACRO_NET_WORK_TYPE_TDSCDMA = 8 + MACRO_NET_WORK_TYPE_HSPA_PLUS = 9 + MACRO_NET_WORK_TYPE_EVDO_REV_0 = 10 + MACRO_NET_WORK_TYPE_EVDO_REV_A = 11 + MACRO_NET_WORK_TYPE_EVDO_REV_B = 12 + MACRO_NET_WORK_TYPE_1xRTT = 13 + MACRO_NET_WORK_TYPE_UMB = 14 + MACRO_NET_WORK_TYPE_1xEVDV = 15 + MACRO_NET_WORK_TYPE_3xRTT = 16 + MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM = 17 + MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO = 18 + MACRO_NET_WORK_TYPE_LTE = 19 + MACRO_NET_WORK_TYPE_EX_NOSERVICE = 0 + MACRO_NET_WORK_TYPE_EX_GSM = 1 + MACRO_NET_WORK_TYPE_EX_GPRS = 2 + MACRO_NET_WORK_TYPE_EX_EDGE = 3 + MACRO_NET_WORK_TYPE_EX_IS95A = 21 + MACRO_NET_WORK_TYPE_EX_IS95B = 22 + MACRO_NET_WORK_TYPE_EX_CDMA_1x = 23 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_0 = 24 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_A = 25 + MACRO_NET_WORK_TYPE_EX_EVDO_REV_B = 26 + MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x = 27 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0 = 28 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A = 29 + MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B = 30 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0 = 31 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A = 32 + MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B = 33 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0 = 34 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A = 35 + MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B = 36 + MACRO_NET_WORK_TYPE_EX_WCDMA = 41 + MACRO_NET_WORK_TYPE_EX_HSDPA = 42 + MACRO_NET_WORK_TYPE_EX_HSUPA = 43 + MACRO_NET_WORK_TYPE_EX_HSPA = 44 + MACRO_NET_WORK_TYPE_EX_HSPA_PLUS = 45 + MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS = 46 + MACRO_NET_WORK_TYPE_EX_TD_SCDMA = 61 + MACRO_NET_WORK_TYPE_EX_TD_HSDPA = 62 + MACRO_NET_WORK_TYPE_EX_TD_HSUPA = 63 + MACRO_NET_WORK_TYPE_EX_TD_HSPA = 64 + MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS = 65 + MACRO_NET_WORK_TYPE_EX_802_16E = 81 + MACRO_NET_WORK_TYPE_EX_LTE = 101 + + +def post_data_to_xml(data: dict, depth: int = 1) -> str: + if depth == 1: + return ''+post_data_to_xml({'request': data}, depth+1) + + items = [] + for k, v in data.items(): + if isinstance(v, dict): + v = post_data_to_xml(v, depth+1) + elif isinstance(v, list): + raise TypeError('list type is unsupported here') + items.append(f'<{k}>{v}') + + return ''.join(items) + + +class E3372: + _addr: Addr + _need_auth: bool + _legacy_token_auth: bool + _get_raw_data: bool + _headers: dict[str, str] + _authorized: bool + + def __init__(self, + addr: Addr, + need_auth: bool = True, + legacy_token_auth: bool = False, + get_raw_data: bool = False): + self._addr = addr + self._need_auth = need_auth + self._legacy_token_auth = legacy_token_auth + self._get_raw_data = get_raw_data + self._authorized = False + self._headers = {} + + @property + def device_information(self): + self.auth() + return self.request('device/information') + + @property + def device_signal(self): + self.auth() + return self.request('device/signal') + + @property + def monitoring_status(self): + self.auth() + return self.request('monitoring/status') + + @property + def notifications(self): + self.auth() + return self.request('monitoring/check-notifications') + + @property + def dialup_connection(self): + self.auth() + return self.request('dialup/connection') + + @property + def traffic_stats(self): + self.auth() + return self.request('monitoring/traffic-statistics') + + @property + def sms_count(self): + self.auth() + return self.request('sms/sms-count') + + def sms_send(self, phone: str, text: str): + self.auth() + return self.request('sms/send-sms', HTTPMethod.POST, { + 'Index': -1, + 'Phones': { + 'Phone': phone + }, + 'Sca': '', + 'Content': text, + 'Length': -1, + 'Reserved': 1, + 'Date': -1 + }) + + def sms_list(self, page: int = 1, count: int = 20, outbox: bool = False): + self.auth() + xml = self.request('sms/sms-list', HTTPMethod.POST, { + 'PageIndex': page, + 'ReadCount': count, + 'BoxType': 1 if not outbox else 2, + 'SortType': 0, + 'Ascending': 0, + 'UnreadPreferred': 1 if not outbox else 0 + }, return_body=True) + + root = ElementTree.fromstring(xml) + messages = [] + for message_elem in root.find('Messages').findall('Message'): + message_dict = {child.tag: child.text for child in message_elem} + messages.append(message_dict) + return messages + + def auth(self): + if self._authorized: + return + + if not self._legacy_token_auth: + data = self.request('webserver/SesTokInfo') + self._headers = { + 'Cookie': data['SesInfo'], + '__RequestVerificationToken': data['TokInfo'], + 'Content-Type': 'text/xml' + } + else: + data = self.request('webserver/token') + self._headers = { + '__RequestVerificationToken': data['token'], + 'Content-Type': 'text/xml' + } + + self._authorized = True + + def request(self, + method: str, + http_method: HTTPMethod = HTTPMethod.GET, + data: dict = {}, + return_body: bool = False) -> Union[str, dict]: + url = f'http://{self._addr}/api/{method}' + if http_method == HTTPMethod.POST: + data = post_data_to_xml(data) + f = requests.post + else: + data = None + f = requests.get + r = f(url, data=data, headers=self._headers) + r.raise_for_status() + r.encoding = 'utf-8' + + if return_body: + return r.text + + root = ElementTree.fromstring(r.text) + data_dict = {} + for elem in root: + data_dict[elem.tag] = elem.text + return data_dict diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 3c73440..f267488 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -12,7 +12,7 @@ import re import os from enum import Enum -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional, List from zlib import adler32 @@ -255,6 +255,25 @@ def filesize_fmt(num, suffix="B") -> str: return f"{num:.1f} Yi{suffix}" +def seconds_to_human_readable_string(seconds: int) -> str: + duration = timedelta(seconds=seconds) + days, remainder = divmod(duration.total_seconds(), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)} day{'s' if days > 1 else ''}") + if hours > 0: + parts.append(f"{int(hours)} hour{'s' if hours > 1 else ''}") + if minutes > 0: + parts.append(f"{int(minutes)} minute{'s' if minutes > 1 else ''}") + if seconds > 0: + parts.append(f"{int(seconds)} second{'s' if seconds > 1 else ''}") + + return ' '.join(parts) + + class HashableEnum(Enum): def hash(self) -> int: return adler32(self.name.encode()) diff --git a/localwebsite/classes/E3372.php b/localwebsite/classes/E3372.php deleted file mode 100644 index a3ce80c..0000000 --- a/localwebsite/classes/E3372.php +++ /dev/null @@ -1,310 +0,0 @@ -host = $host; - $this->useLegacyTokenAuth = $legacy_token_auth; - } - - public function auth() { - if ($this->authorized) - return; - - if (!$this->useLegacyTokenAuth) { - $data = $this->request('webserver/SesTokInfo'); - $this->headers = [ - 'Cookie: '.$data['SesInfo'], - '__RequestVerificationToken: '.$data['TokInfo'], - 'Content-Type: text/xml' - ]; - } else { - $data = $this->request('webserver/token'); - $this->headers = [ - '__RequestVerificationToken: '.$data['token'], - 'Content-Type: text/xml' - ]; - } - $this->authorized = true; - } - - public function getDeviceInformation() { - $this->auth(); - return $this->request('device/information'); - } - - public function getDeviceSignal() { - $this->auth(); - return $this->request('device/signal'); - } - - public function getMonitoringStatus() { - $this->auth(); - return $this->request('monitoring/status'); - } - - public function getNotifications() { - $this->auth(); - return $this->request('monitoring/check-notifications'); - } - - public function getDialupConnection() { - $this->auth(); - return $this->request('dialup/connection'); - } - - public function getTrafficStats() { - $this->auth(); - return $this->request('monitoring/traffic-statistics'); - } - - public function getSMSCount() { - $this->auth(); - return $this->request('sms/sms-count'); - } - - public function sendSMS(string $phone, string $text) { - $this->auth(); - return $this->request('sms/send-sms', 'POST', [ - 'Index' => -1, - 'Phones' => [ - 'Phone' => $phone - ], - 'Sca' => '', - 'Content' => $text, - 'Length' => -1, - 'Reserved' => 1, - 'Date' => -1 - ]); - } - - public function getSMSList(int $page = 1, int $count = 20, bool $outbox = false) { - $this->auth(); - $xml = $this->request('sms/sms-list', 'POST', [ - 'PageIndex' => $page, - 'ReadCount' => $count, - 'BoxType' => !$outbox ? 1 : 2, - 'SortType' => 0, - 'Ascending' => 0, - 'UnreadPreferred' => !$outbox ? 1 : 0 - ], true); - $xml = simplexml_load_string($xml); - - $messages = []; - foreach ($xml->Messages->Message as $message) { - $dt = DateTime::createFromFormat("Y-m-d H:i:s", (string)$message->Date); - $messages[] = [ - 'date' => (string)$message->Date, - 'timestamp' => $dt->getTimestamp(), - 'phone' => (string)$message->Phone, - 'content' => (string)$message->Content - ]; - } - return $messages; - } - - private function xmlToAssoc(string $xml): array { - $xml = new SimpleXMLElement($xml); - $data = []; - foreach ($xml as $name => $value) { - $data[$name] = (string)$value; - } - return $data; - } - - private function request(string $method, string $http_method = 'GET', array $data = [], bool $return_body = false) { - $ch = curl_init(); - $url = 'http://'.$this->host.'/api/'.$method; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - if (!empty($this->headers)) - curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); - if ($http_method == 'POST') { - curl_setopt($ch, CURLOPT_POST, true); - - $post_data = $this->postDataToXML($data); - // debugLog('post_data:', $post_data); - - if (!empty($data)) - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - } - $body = curl_exec($ch); - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($code != 200) - throw new Exception('e3372 host returned code '.$code); - - curl_close($ch); - return $return_body ? $body : $this->xmlToAssoc($body); - } - - private function postDataToXML(array $data, int $depth = 1): string { - if ($depth == 1) - return ''.$this->postDataToXML(['request' => $data], $depth+1); - - $items = []; - foreach ($data as $key => $value) { - if (is_array($value)) - $value = $this->postDataToXML($value, $depth+1); - $items[] = "<{$key}>{$value}"; - } - - return implode('', $items); - } - - public static function getNetworkTypeLabel($type): string { - switch ((int)$type) { - case self::MACRO_NET_WORK_TYPE_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_TDSCDMA: return 'TDSCDMA'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_1xRTT: return '1xRTT'; - case self::MACRO_NET_WORK_TYPE_UMB: return 'UMB'; - case self::MACRO_NET_WORK_TYPE_1xEVDV: return '1xEVDV'; - case self::MACRO_NET_WORK_TYPE_3xRTT: return '3xRTT'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: return 'HSPA_PLUS_64QAM'; - case self::MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: return 'HSPA_PLUS_MIMO'; - case self::MACRO_NET_WORK_TYPE_LTE: return 'LTE'; - case self::MACRO_NET_WORK_TYPE_EX_NOSERVICE: return 'NOSERVICE'; - case self::MACRO_NET_WORK_TYPE_EX_GSM: return 'GSM'; - case self::MACRO_NET_WORK_TYPE_EX_GPRS: return 'GPRS'; - case self::MACRO_NET_WORK_TYPE_EX_EDGE: return 'EDGE'; - case self::MACRO_NET_WORK_TYPE_EX_IS95A: return 'IS95A'; - case self::MACRO_NET_WORK_TYPE_EX_IS95B: return 'IS95B'; - case self::MACRO_NET_WORK_TYPE_EX_CDMA_1x: return 'CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: return 'EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: return 'EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: return 'EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1x: return 'HYBRID_CDMA_1x'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: return 'HYBRID_EVDO_REV_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: return 'HYBRID_EVDO_REV_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: return 'HYBRID_EVDO_REV_B'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: return 'EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: return 'EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: return 'EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: return 'HYBRID_EHRPD_REL_0'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: return 'HYBRID_EHRPD_REL_A'; - case self::MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: return 'HYBRID_EHRPD_REL_B'; - case self::MACRO_NET_WORK_TYPE_EX_WCDMA: return 'WCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_HSDPA: return 'HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSUPA: return 'HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA: return 'HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: return 'HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: return 'DC_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_TD_SCDMA: return 'TD_SCDMA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSDPA: return 'TD_HSDPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSUPA: return 'TD_HSUPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA: return 'TD_HSPA'; - case self::MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: return 'TD_HSPA_PLUS'; - case self::MACRO_NET_WORK_TYPE_EX_802_16E: return '802_16E'; - case self::MACRO_NET_WORK_TYPE_EX_LTE: return 'LTE'; - default: return '?'; - } - } - -} diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index 6743fe9..8179620 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -7,65 +7,6 @@ use libphonenumber\PhoneNumberUtil; class ModemHandler extends RequestHandler { - public function GET_status_page() { - global $config; - - $this->tpl->set([ - 'modems' => $config['modems'], - 'js_modems' => array_keys($config['modems']), - ]); - - $this->tpl->set_title('Состояние модемов'); - $this->tpl->render_page('modem_status_page.twig'); - } - - public function GET_status_get_ajax() { - global $config; - list($id) = $this->input('id'); - if (!isset($config['modems'][$id])) - ajax_error('invalid modem id: '.$id); - - $modem_data = self::getModemData( - $config['modems'][$id]['ip'], - $config['modems'][$id]['legacy_token_auth']); - - ajax_ok([ - 'html' => $this->tpl->render('modem_data.twig', [ - 'loading' => false, - 'modem' => $id, - 'modem_data' => $modem_data - ]) - ]); - } - - public function GET_verbose_page() { - global $config; - - list($modem) = $this->input('modem'); - if (!$modem) - $modem = array_key_first($config['modems']); - - list($signal, $status, $traffic, $device, $dialup_conn) = self::getModemData( - $config['modems'][$modem]['ip'], - $config['modems'][$modem]['legacy_token_auth'], - true); - - $data = [ - ['Signal', $signal], - ['Connection', $status], - ['Traffic', $traffic], - ['Device info', $device], - ['Dialup connection', $dialup_conn] - ]; - $this->tpl->set([ - 'data' => $data, - 'modem_name' => $config['modems'][$modem]['label'], - ]); - $this->tpl->set_title('Подробная информация о модеме '.$modem); - $this->tpl->render_page('modem_verbose_page.twig'); - } - - public function GET_routing_smallhome_page() { global $config; @@ -233,32 +174,6 @@ class ModemHandler extends RequestHandler $go_back(); } - protected static function getModemData(string $ip, - bool $need_auth = true, - bool $get_raw_data = false): array { - $modem = new E3372($ip, $need_auth); - - $signal = $modem->getDeviceSignal(); - $status = $modem->getMonitoringStatus(); - $traffic = $modem->getTrafficStats(); - - if ($get_raw_data) { - $device_info = $modem->getDeviceInformation(); - $dialup_conn = $modem->getDialupConnection(); - return [$signal, $status, $traffic, $device_info, $dialup_conn]; - } else { - return [ - 'type' => e3372::getNetworkTypeLabel($status['CurrentNetworkType']), - 'level' => $status['SignalIcon'] ?? 0, - 'rssi' => $signal['rssi'], - 'sinr' => $signal['sinr'], - 'connected_time' => secondsToTime($traffic['CurrentConnectTime']), - 'downloaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentDownload'])), - 'uploaded' => bytesToUnitsLabel(gmp_init($traffic['CurrentUpload'])), - ]; - } - } - protected static function getCurrentUpstream() { global $config; diff --git a/localwebsite/templates-web/modem_data.twig b/localwebsite/templates-web/modem_data.twig deleted file mode 100644 index a2c00e5..0000000 --- a/localwebsite/templates-web/modem_data.twig +++ /dev/null @@ -1,14 +0,0 @@ -{% if not loading %} - Сигнал: {% include 'signal_level.twig' with {'level': modem_data.level} %}
    - Тип сети: {{ modem_data.type }}
    - RSSI: {{ modem_data.rssi }}
    - {% if modem_data.sinr %} - SINR: {{ modem_data.sinr }}
    - {% endif %} - Время соединения: {{ modem_data.connected_time }}
    - Принято/передано: {{ modem_data.downloaded }} / {{ modem_data.uploaded }} -
    - Подробная информация -{% else %} - {% include 'spinner.twig' %} -{% endif %} \ No newline at end of file diff --git a/localwebsite/templates-web/modem_status_page.twig b/localwebsite/templates-web/modem_status_page.twig deleted file mode 100644 index 3f20b86..0000000 --- a/localwebsite/templates-web/modem_status_page.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {text: "Модемы" } - ] -} %} - -{% for modem_key, modem in modems %} -
    {{ modem.label }}
    -
    - {% include 'modem_data.twig' with { - loading: true, - modem: modem_key - } %} -
    -{% endfor %} - -{% js %} -ModemStatus.init({{ js_modems|json_encode|raw }}); -{% endjs %} diff --git a/localwebsite/templates-web/modem_verbose_page.twig b/localwebsite/templates-web/modem_verbose_page.twig deleted file mode 100644 index 3b4c25e..0000000 --- a/localwebsite/templates-web/modem_verbose_page.twig +++ /dev/null @@ -1,15 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {link: '/modem/', text: "Модемы" }, - {text: modem_name} - ] -} %} - -{% for item in data %} - {% set item_name = item[0] %} - {% set item_data = item[1] %} -
    {{ item_name }}
    - {% for k, v in item_data %} - {{ k }} = {{ v }}
    - {% endfor %} -{% endfor %} \ No newline at end of file diff --git a/localwebsite/templates-web/spinner.twig b/localwebsite/templates-web/spinner.twig deleted file mode 100644 index 2d629ea..0000000 --- a/localwebsite/templates-web/spinner.twig +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/test/test_modems.py b/test/test_modems.py new file mode 100755 index 0000000..39981f7 --- /dev/null +++ b/test/test_modems.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import __py_include +from homekit.modem import E3372, ModemsConfig + + +if __name__ == '__main__': + mc = ModemsConfig() + modem = mc.get('mts-azov') + cl = E3372(modem['ip'], legacy_token_auth=modem['legacy_auth']) diff --git a/web/kbn_assets/app.css b/web/kbn_assets/app.css index 3146bcf..1a4697a 100644 --- a/web/kbn_assets/app.css +++ b/web/kbn_assets/app.css @@ -14,7 +14,7 @@ } -/** spinner.twig **/ +/** spinner.j2 **/ .sk-fading-circle { margin-top: 10px; diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js index c187f89..eaac003 100644 --- a/web/kbn_assets/app.js +++ b/web/kbn_assets/app.js @@ -319,6 +319,26 @@ window.Cameras = { })(); +class ModemStatusUpdater { + constructor(id) { + this.id = id; + this.elem = ge('modem_data_'+id); + this.fetch() + } + + fetch() { + ajax.get('/modems/info.ajx', { + id: this.id + }).then(({response}) => { + const {html} = response; + this.elem.innerHTML = html; + + // TODO enqueue rerender + }); + } +} + + var ModemStatus = { _modems: [], @@ -329,21 +349,3 @@ var ModemStatus = { } } }; - -function ModemStatusUpdater(id) { - this.id = id; - this.elem = ge('modem_data_'+id); - this.fetch(); -} -extend(ModemStatusUpdater.prototype, { - fetch: function() { - ajax.get('/modem/get.ajax', { - id: this.id - }).then(({response}) => { - var {html} = response; - this.elem.innerHTML = html; - - // TODO enqueue rerender - }); - }, -}); \ No newline at end of file diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 index d43a08b..e2e29e3 100644 --- a/web/kbn_templates/base.j2 +++ b/web/kbn_templates/base.j2 @@ -35,9 +35,9 @@ {% block content %}{% endblock %} -{% if js %} - -{% endif %} +
    diff --git a/web/kbn_templates/modem_data.j2 b/web/kbn_templates/modem_data.j2 new file mode 100644 index 0000000..7f97b77 --- /dev/null +++ b/web/kbn_templates/modem_data.j2 @@ -0,0 +1,13 @@ +{% with level=modem_data.level %} + Сигнал: {% include 'signal_level.j2' %}
    +{% endwith %} + +Тип сети: {{ modem_data.type }}
    +RSSI: {{ modem_data.rssi }}
    +{% if modem_data.sinr %} +SINR: {{ modem_data.sinr }}
    +{% endif %} +Время соединения: {{ modem_data.connected_time }}
    +Принято/передано: {{ modem_data.downloaded }} / {{ modem_data.uploaded }} +
    +Подробная информация diff --git a/web/kbn_templates/modem_verbose.j2 b/web/kbn_templates/modem_verbose.j2 new file mode 100644 index 0000000..7c6c930 --- /dev/null +++ b/web/kbn_templates/modem_verbose.j2 @@ -0,0 +1,18 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([ + {'link': '/modems.cgi', 'text': "Модемы"}, + {'text': modem_name} +]) }} + +{% for item in data %} + {% set item_name = item[0] %} + {% set item_data = item[1] %} +
    {{ item_name }}
    + {% for k, v in item_data.items() %} + {{ k }} = {{ v }}
    + {% endfor %} +{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/web/kbn_templates/modems.j2 b/web/kbn_templates/modems.j2 index f148140..4ff9cf8 100644 --- a/web/kbn_templates/modems.j2 +++ b/web/kbn_templates/modems.j2 @@ -9,4 +9,8 @@ {% include "loading.j2" %} {% endfor %} +{% endblock %} + +{% block js %} +ModemStatus.init({{ modems.getkeys()|tojson }}); {% endblock %} \ No newline at end of file diff --git a/localwebsite/templates-web/signal_level.twig b/web/kbn_templates/signal_level.j2 similarity index 80% rename from localwebsite/templates-web/signal_level.twig rename to web/kbn_templates/signal_level.j2 index 9498482..93c9abf 100644 --- a/localwebsite/templates-web/signal_level.twig +++ b/web/kbn_templates/signal_level.j2 @@ -1,5 +1,5 @@
    - {% for i in 0..4 %} + {% for i in range(5) %}
    {% endfor %} \ No newline at end of file From de56aa3ae916ac0d51e503648fae8f3fa2d97951 Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Tue, 16 Jan 2024 02:10:58 +0300 Subject: [PATCH 47/51] save --- bin/web_kbn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 397841d..113554e 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -11,6 +11,7 @@ from io import StringIO from typing import Optional, Union from homekit.config import config, AppConfigUnit from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string +from homekit.modem import E3372, ModemsConfig, MacroNetWorkType from aiohttp import web from homekit import http From 8a89dd77be03ca8eb9cdc378ba8e912292494fa9 Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Tue, 16 Jan 2024 03:31:55 +0300 Subject: [PATCH 48/51] inverter page --- bin/web_kbn.py | 108 ++++++++++++++++-- include/py/homekit/http/http.py | 5 +- include/py/homekit/inverter/config.py | 4 +- localwebsite/handlers/InverterHandler.php | 104 ----------------- localwebsite/htdocs/index.php | 7 -- localwebsite/templates-web/inverter_page.twig | 20 ---- web/kbn_assets/app.js | 17 +++ web/kbn_assets/inverter.js | 15 --- web/kbn_templates/inverter.j2 | 20 ++++ 9 files changed, 141 insertions(+), 159 deletions(-) delete mode 100644 localwebsite/handlers/InverterHandler.php delete mode 100644 localwebsite/templates-web/inverter_page.twig delete mode 100644 web/kbn_assets/inverter.js create mode 100644 web/kbn_templates/inverter.j2 diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 113554e..d9d0035 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -3,16 +3,17 @@ import asyncio import jinja2 import aiohttp_jinja2 import json -import os import re +import inverterd import __py_include from io import StringIO +from aiohttp.web import HTTPFound from typing import Optional, Union from homekit.config import config, AppConfigUnit from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string from homekit.modem import E3372, ModemsConfig, MacroNetWorkType -from aiohttp import web +from homekit.inverter.config import InverterdConfig from homekit import http @@ -90,6 +91,69 @@ def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: } +def get_inverter_client() -> inverterd.Client: + cl = inverterd.Client(host=InverterdConfig()['remote_addr'].host) + cl.connect() + cl.format(inverterd.Format.JSON) + return cl + + +def get_inverter_data() -> tuple: + cl = get_inverter_client() + + status = json.loads(cl.exec('get-status'))['data'] + rated = json.loads(cl.exec('get-rated'))['data'] + + power_direction = status['battery_power_direction'].lower() + power_direction = re.sub('ge$', 'ging', power_direction) + + charging_rate = '' + if power_direction == 'charging': + charging_rate = ' @ %s %s' % ( + status['battery_charge_current']['value'], + status['battery_charge_current']['unit']) + elif power_direction == 'discharging': + charging_rate = ' @ %s %s' % ( + status['battery_discharge_current']['value'], + status['battery_discharge_current']['unit']) + + html = 'Battery: %s %s' % ( + status['battery_voltage']['value'], + status['battery_voltage']['unit']) + html += ' (%s%s, ' % ( + status['battery_capacity']['value'], + status['battery_capacity']['unit']) + html += '%s%s)' % (power_direction, charging_rate) + + html += "\n" + html += 'Load: %s %s' % ( + status['ac_output_active_power']['value'], + status['ac_output_active_power']['unit']) + html += ' (%s%%)' % (status['output_load_percent']['value'],) + + if status['pv1_input_power']['value'] > 0: + html += "\n" + html += 'Input power: %s %s' % ( + status['pv1_input_power']['value'], + status['pv1_input_power']['unit']) + + if status['grid_voltage']['value'] > 0 or status['grid_freq']['value'] > 0: + html += "\n" + html += 'AC input: %s %s' % ( + status['grid_voltage']['value'], + status['grid_voltage']['unit']) + html += ', %s %s' % ( + status['grid_freq']['value'], + status['grid_freq']['unit']) + + html += "\n" + html += 'Priority: %s' % (rated['output_source_priority'],) + + html = html.replace("\n", '
    ') + + return status, rated, html + + class WebSite(http.HTTPServer): _modems_config: ModemsConfig @@ -108,10 +172,14 @@ class WebSite(http.HTTPServer): self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) - self.get('/main.cgi', self.get_index) - self.get('/modems.cgi', self.get_modems) - self.get('/modems/info.ajx', self.get_modems_ajax) - self.get('/modems/verbose.cgi', self.get_modems_verbose) + self.get('/main.cgi', self.index) + + self.get('/modems.cgi', self.modems) + self.get('/modems/info.ajx', self.modems_ajx) + self.get('/modems/verbose.cgi', self.modems_verbose) + + self.get('/inverter.cgi', self.inverter) + self.get('/inverter.ajx', self.inverter_ajx) async def render_page(self, req: http.Request, @@ -129,16 +197,16 @@ class WebSite(http.HTTPServer): response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context) return response - async def get_index(self, req: http.Request): + async def index(self, req: http.Request): return await self.render_page(req, 'index', title="Home web site") - async def get_modems(self, req: http.Request): + async def modems(self, req: http.Request): return await self.render_page(req, 'modems', title='Состояние модемов', context=dict(modems=self._modems_config)) - async def get_modems_ajax(self, req: http.Request): + async def modems_ajx(self, req: http.Request): modem = req.query.get('id', None) if modem not in self._modems_config.getkeys(): raise ValueError('invalid modem id') @@ -154,7 +222,7 @@ class WebSite(http.HTTPServer): return self.ok({'html': html}) - async def get_modems_verbose(self, req: http.Request): + async def modems_verbose(self, req: http.Request): modem = req.query.get('id', None) if modem not in self._modems_config.getkeys(): raise ValueError('invalid modem id') @@ -175,6 +243,26 @@ class WebSite(http.HTTPServer): title=f'Подробная информация о модеме "{modem_name}"', context=dict(data=data, modem_name=modem_name)) + async def inverter(self, req: http.Request): + action = req.query.get('do', None) + if action == 'set-osp': + val = req.query.get('value') + if val not in ('sub', 'sbu'): + raise ValueError('invalid osp value') + cl = get_inverter_client() + cl.exec('set-output-source-priority', + arguments=(val.upper(),)) + raise HTTPFound('/inverter.cgi') + + status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data) + return await self.render_page(req, 'inverter', + title='Инвертор', + context=dict(status=status, rated=rated, html=html)) + + async def inverter_ajx(self, req: http.Request): + status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data) + return self.ok({'html': html}) + if __name__ == '__main__': config.load_app(WebKbnConfig) diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py index 9b76d9a..82c5aae 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -3,7 +3,7 @@ import asyncio from enum import Enum from aiohttp import web -from aiohttp.web import Response +from aiohttp.web import Response, HTTPFound from aiohttp.web_exceptions import HTTPNotFound from ..util import stringify, format_tb, Addr @@ -21,6 +21,9 @@ async def errors_handler_middleware(request, handler): except HTTPNotFound: return web.json_response({'error': 'not found'}, status=404) + except HTTPFound as exc: + raise exc + except Exception as exc: _logger.exception(exc) data = { diff --git a/include/py/homekit/inverter/config.py b/include/py/homekit/inverter/config.py index e284dfe..694ddae 100644 --- a/include/py/homekit/inverter/config.py +++ b/include/py/homekit/inverter/config.py @@ -8,6 +8,6 @@ class InverterdConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'remote_addr': {'type': 'string'}, - 'local_addr': {'type': 'string'}, + 'remote_addr': cls._addr_schema(required=True), + 'local_addr': cls._addr_schema(required=True), } \ No newline at end of file diff --git a/localwebsite/handlers/InverterHandler.php b/localwebsite/handlers/InverterHandler.php deleted file mode 100644 index 5fa269f..0000000 --- a/localwebsite/handlers/InverterHandler.php +++ /dev/null @@ -1,104 +0,0 @@ -tpl->add_static('inverter.js'); - } - - public function GET_status_page() { - $inv = $this->getClient(); - - $status = jsonDecode($inv->exec('get-status'))['data']; - $rated = jsonDecode($inv->exec('get-rated'))['data']; - - $this->tpl->set([ - 'status' => $status, - 'rated' => $rated, - 'html' => $this->renderStatusHtml($status, $rated) - ]); - $this->tpl->set_title('Инвертор'); - $this->tpl->render_page('inverter_page.twig'); - } - - public function GET_set_osp() { - list($osp) = $this->input('e:value(=sub|sbu)'); - $inv = $this->getClient(); - try { - $inv->exec('set-output-source-priority', [strtoupper($osp)]); - } catch (Exception $e) { - die('Ошибка: '.jsonDecode($e->getMessage())['message']); - } - redirect('/inverter/'); - } - - public function GET_status_ajax() { - $inv = $this->getClient(); - $status = jsonDecode($inv->exec('get-status'))['data']; - $rated = jsonDecode($inv->exec('get-rated'))['data']; - ajax_ok(['html' => $this->renderStatusHtml($status, $rated)]); - } - - protected function renderStatusHtml(array $status, array $rated) { - $power_direction = strtolower($status['battery_power_direction']); - $power_direction = preg_replace('/ge$/', 'ging', $power_direction); - - $charging_rate = ''; - if ($power_direction == 'charging') - $charging_rate = sprintf(' @ %s %s', - $status['battery_charge_current']['value'], - $status['battery_charge_current']['unit']); - else if ($power_direction == 'discharging') - $charging_rate = sprintf(' @ %s %s', - $status['battery_discharge_current']['value'], - $status['battery_discharge_current']['unit']); - - $html = sprintf('Battery: %s %s', - $status['battery_voltage']['value'], - $status['battery_voltage']['unit']); - $html .= sprintf(' (%s%s, ', - $status['battery_capacity']['value'], - $status['battery_capacity']['unit']); - $html .= sprintf('%s%s)', - $power_direction, - $charging_rate); - - $html .= "\n".sprintf('Load: %s %s', - $status['ac_output_active_power']['value'], - $status['ac_output_active_power']['unit']); - $html .= sprintf(' (%s%%)', - $status['output_load_percent']['value']); - - if ($status['pv1_input_power']['value'] > 0) - $html .= "\n".sprintf('Input power: %s %s', - $status['pv1_input_power']['value'], - $status['pv1_input_power']['unit']); - - if ($status['grid_voltage']['value'] > 0 or $status['grid_freq']['value'] > 0) { - $html .= "\n".sprintf('AC input: %s %s', - $status['grid_voltage']['value'], - $status['grid_voltage']['unit']); - $html .= sprintf(', %s %s', - $status['grid_freq']['value'], - $status['grid_freq']['unit']); - } - - $html .= "\n".sprintf('Priority: %s', - $rated['output_source_priority']); - - return nl2br($html); - } - - protected function getClient(): InverterdClient { - global $config; - if (isset($_GET['alt']) && $_GET['alt'] == 1) - $config['inverterd_host'] = '192.168.5.223'; - $inv = new InverterdClient($config['inverterd_host'], $config['inverterd_port']); - $inv->setFormat('json'); - return $inv; - } - - -} diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php index d6034e6..eeeaacb 100644 --- a/localwebsite/htdocs/index.php +++ b/localwebsite/htdocs/index.php @@ -4,11 +4,6 @@ require_once __DIR__.'/../init.php'; $router = new router; -// modem -$router->add('modem/', 'Modem status_page'); -$router->add('modem/verbose/', 'Modem verbose_page'); -$router->add('modem/get.ajax', 'Modem status_get_ajax'); - $router->add('routing/', 'Modem routing_smallhome_page'); $router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch'); $router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page'); @@ -18,9 +13,7 @@ $router->add('sms/', 'Modem sms'); // $router->add('modem/set.ajax', 'Modem ctl_set_ajax'); // inverter -$router->add('inverter/', 'Inverter status_page'); $router->add('inverter/set-osp/', 'Inverter set_osp'); -$router->add('inverter/status.ajax', 'Inverter status_ajax'); // misc $router->add('/', 'Misc main'); diff --git a/localwebsite/templates-web/inverter_page.twig b/localwebsite/templates-web/inverter_page.twig deleted file mode 100644 index c51e1bf..0000000 --- a/localwebsite/templates-web/inverter_page.twig +++ /dev/null @@ -1,20 +0,0 @@ -{% include 'bc.twig' with { - history: [ - {text: "Инвертор" } - ] -} %} - -
    Статус
    -
    - {{ html|raw }} -
    - - - -{% js %} -Inverter.poll(); -{% endjs %} \ No newline at end of file diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js index eaac003..d575a5a 100644 --- a/web/kbn_assets/app.js +++ b/web/kbn_assets/app.js @@ -349,3 +349,20 @@ var ModemStatus = { } } }; + + +var Inverter = { + poll: function () { + setInterval(this._tick, 1000); + }, + + _tick: function() { + ajax.get('/inverter.ajx') + .then(({response}) => { + if (response) { + var el = document.getElementById('inverter_status'); + el.innerHTML = response.html; + } + }); + } +}; \ No newline at end of file diff --git a/web/kbn_assets/inverter.js b/web/kbn_assets/inverter.js deleted file mode 100644 index 72d985c..0000000 --- a/web/kbn_assets/inverter.js +++ /dev/null @@ -1,15 +0,0 @@ -var Inverter = { - poll: function () { - setInterval(this._tick, 1000); - }, - - _tick: function() { - ajax.get('/inverter/status.ajax') - .then(({response}) => { - if (response) { - var el = document.getElementById('inverter_status'); - el.innerHTML = response.html; - } - }); - } -}; \ No newline at end of file diff --git a/web/kbn_templates/inverter.j2 b/web/kbn_templates/inverter.j2 new file mode 100644 index 0000000..26491f3 --- /dev/null +++ b/web/kbn_templates/inverter.j2 @@ -0,0 +1,20 @@ +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'Инвертор'}]) }} + +
    Статус
    +
    + {{ html|safe }} +
    + + +{% endblock %} + +{% block js %} +Inverter.poll(); +{% endblock %} \ No newline at end of file From a9a241ad19449c29b68cd4a5b539bcbec816e341 Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Wed, 17 Jan 2024 03:35:59 +0300 Subject: [PATCH 49/51] lws: pump page rewritten to python --- bin/web_kbn.py | 29 +++++++- include/py/homekit/util.py | 5 +- localwebsite/classes/GPIORelaydClient.php | 18 ----- localwebsite/classes/InverterdClient.php | 69 ------------------- localwebsite/handlers/MiscHandler.php | 42 ----------- localwebsite/htdocs/index.php | 2 - .../pump.twig => web/kbn_templates/pump.j2 | 16 ++--- 7 files changed, 38 insertions(+), 143 deletions(-) delete mode 100644 localwebsite/classes/GPIORelaydClient.php delete mode 100644 localwebsite/classes/InverterdClient.php rename localwebsite/templates-web/pump.twig => web/kbn_templates/pump.j2 (61%) diff --git a/bin/web_kbn.py b/bin/web_kbn.py index d9d0035..09fa9c6 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -14,6 +14,7 @@ from homekit.config import config, AppConfigUnit from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string from homekit.modem import E3372, ModemsConfig, MacroNetWorkType from homekit.inverter.config import InverterdConfig +from homekit.relay.sunxi_h3_client import RelayClient from homekit import http @@ -24,7 +25,8 @@ class WebKbnConfig(AppConfigUnit): def schema(cls) -> Optional[dict]: return { 'listen_addr': cls._addr_schema(required=True), - 'assets_public_path': {'type': 'string'} + 'assets_public_path': {'type': 'string'}, + 'pump_addr': cls._addr_schema(required=True), } @@ -91,6 +93,13 @@ def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: } +def get_pump_client() -> RelayClient: + addr = config.app_config['pump_addr'] + cl = RelayClient(host=addr.host, port=addr.port) + cl.connect() + return cl + + def get_inverter_client() -> inverterd.Client: cl = inverterd.Client(host=InverterdConfig()['remote_addr'].host) cl.connect() @@ -180,6 +189,7 @@ class WebSite(http.HTTPServer): self.get('/inverter.cgi', self.inverter) self.get('/inverter.ajx', self.inverter_ajx) + self.get('/pump.cgi', self.pump) async def render_page(self, req: http.Request, @@ -263,6 +273,23 @@ class WebSite(http.HTTPServer): status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data) return self.ok({'html': html}) + async def pump(self, req: http.Request): + # TODO + # these are blocking calls + # should be rewritten using aio + + cl = get_pump_client() + + action = req.query.get('set', None) + if action in ('on', 'off'): + getattr(cl, action)() + raise HTTPFound('/pump.cgi') + + status = cl.status() + return await self.render_page(req, 'pump', + title='Насос', + context=dict(status=status)) + if __name__ == '__main__': config.load_app(WebKbnConfig) diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index f267488..78a78a0 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -12,7 +12,7 @@ import re import os from enum import Enum -from datetime import datetime, timedelta +from datetime import datetime from typing import Optional, List from zlib import adler32 @@ -256,8 +256,7 @@ def filesize_fmt(num, suffix="B") -> str: def seconds_to_human_readable_string(seconds: int) -> str: - duration = timedelta(seconds=seconds) - days, remainder = divmod(duration.total_seconds(), 86400) + days, remainder = divmod(seconds, 86400) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) diff --git a/localwebsite/classes/GPIORelaydClient.php b/localwebsite/classes/GPIORelaydClient.php deleted file mode 100644 index 89c8dc9..0000000 --- a/localwebsite/classes/GPIORelaydClient.php +++ /dev/null @@ -1,18 +0,0 @@ -send($status); - return $this->recv(); - } - - public function getStatus() { - $this->send('get'); - return $this->recv(); - } - -} \ No newline at end of file diff --git a/localwebsite/classes/InverterdClient.php b/localwebsite/classes/InverterdClient.php deleted file mode 100644 index b68b784..0000000 --- a/localwebsite/classes/InverterdClient.php +++ /dev/null @@ -1,69 +0,0 @@ -send("v $v"); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function setFormat(string $fmt): string - { - $this->send("format $fmt"); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function exec(string $command, array $arguments = []): string - { - $buf = "exec $command"; - if (!empty($arguments)) { - foreach ($arguments as $arg) - $buf .= " $arg"; - } - $this->send($buf); - return $this->recv(); - } - - /** - * @throws Exception - */ - public function recv() - { - $recv_buf = ''; - $buf = ''; - - while (true) { - $result = socket_recv($this->sock, $recv_buf, 1024, 0); - if ($result === false) - throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError()); - - // peer disconnected - if ($result === 0) - break; - - $buf .= $recv_buf; - if (endsWith($buf, "\r\n\r\n")) - break; - } - - $response = explode("\r\n", $buf); - $status = array_shift($response); - if (!in_array($status, ['ok', 'err'])) - throw new Exception(__METHOD__.': unexpected status ('.$status.')'); - if ($status == 'err') - throw new Exception(empty($response) ? 'unknown inverterd error' : $response[0]); - - return trim(implode("\r\n", $response)); - } - -} \ No newline at end of file diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php index 4c5a25e..efaca22 100644 --- a/localwebsite/handlers/MiscHandler.php +++ b/localwebsite/handlers/MiscHandler.php @@ -3,17 +3,6 @@ class MiscHandler extends RequestHandler { - public function GET_main() { - global $config; - $this->tpl->set_title('Главная'); - $this->tpl->set([ - 'grafana_sensors_url' => $config['grafana_sensors_url'], - 'grafana_inverter_url' => $config['grafana_inverter_url'], - 'cameras' => $config['cam_list']['labels'] - ]); - $this->tpl->render_page('index.twig'); - } - public function GET_sensors_page() { global $config; @@ -30,29 +19,6 @@ class MiscHandler extends RequestHandler $this->tpl->render_page('sensors.twig'); } - public function GET_pump_page() { - global $config; - - if (isset($_GET['alt']) && $_GET['alt'] == 1) - $config['pump_host'] = '192.168.5.223'; - - list($set) = $this->input('set'); - $client = new GPIORelaydClient($config['pump_host'], $config['pump_port']); - - if ($set == GPIORelaydClient::STATUS_ON || $set == GPIORelaydClient::STATUS_OFF) { - $client->setStatus($set); - redirect('/pump/'); - } - - $status = $client->getStatus(); - - $this->tpl->set([ - 'status' => $status - ]); - $this->tpl->set_title('Насос'); - $this->tpl->render_page('pump.twig'); - } - public function GET_cams() { global $config; @@ -160,12 +126,4 @@ class MiscHandler extends RequestHandler } } - public function GET_debug() { - print_r($_SERVER); - } - - public function GET_phpinfo() { - phpinfo(); - } - } diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php index eeeaacb..cd32132 100644 --- a/localwebsite/htdocs/index.php +++ b/localwebsite/htdocs/index.php @@ -18,8 +18,6 @@ $router->add('inverter/set-osp/', 'Inverter set_osp'); // misc $router->add('/', 'Misc main'); $router->add('sensors/', 'Misc sensors_page'); -$router->add('pump/', 'Misc pump_page'); -$router->add('phpinfo/', 'Misc phpinfo'); $router->add('cams/', 'Misc cams'); $router->add('cams/([\d,]+)/', 'Misc cams id=$(1)'); $router->add('cams/stat/', 'Misc cams_stat'); diff --git a/localwebsite/templates-web/pump.twig b/web/kbn_templates/pump.j2 similarity index 61% rename from localwebsite/templates-web/pump.twig rename to web/kbn_templates/pump.j2 index 3bce0e2..28d5c9d 100644 --- a/localwebsite/templates-web/pump.twig +++ b/web/kbn_templates/pump.j2 @@ -1,11 +1,10 @@ -{% include 'bc.twig' with { - history: [ - {text: "Насос" } - ] -} %} +{% extends "base.j2" %} -
    - +{% block content %} +{{ breadcrumbs([{'text': 'Насос'}]) }} + + + Сейчас насос {% if status == 'on' %} включен.

    @@ -14,4 +13,5 @@ выключен.

    {% endif %} -
    \ No newline at end of file + +{% endblock %} From d237e81873a9e043f579e7f6a979f00510ddce08 Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Thu, 18 Jan 2024 04:14:38 +0300 Subject: [PATCH 50/51] lws: sms page rewrite --- bin/web_kbn.py | 64 ++++++++++++++- include/py/homekit/config/config.py | 10 ++- localwebsite/handlers/ModemHandler.php | 79 ------------------- requirements.txt | 1 + web/kbn_templates/index.j2 | 4 +- .../sms_page.twig => web/kbn_templates/sms.j2 | 31 ++++---- 6 files changed, 87 insertions(+), 102 deletions(-) rename localwebsite/templates-web/sms_page.twig => web/kbn_templates/sms.j2 (73%) diff --git a/bin/web_kbn.py b/bin/web_kbn.py index 09fa9c6..c21269b 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -5,6 +5,7 @@ import aiohttp_jinja2 import json import re import inverterd +import phonenumbers import __py_include from io import StringIO @@ -27,6 +28,8 @@ class WebKbnConfig(AppConfigUnit): 'listen_addr': cls._addr_schema(required=True), 'assets_public_path': {'type': 'string'}, 'pump_addr': cls._addr_schema(required=True), + 'inverter_grafana_url': {'type': 'string'}, + 'sensors_grafana_url': {'type': 'string'}, } @@ -69,8 +72,12 @@ def get_head_static() -> str: return buf.getvalue() +def get_modem_client(modem_cfg: dict) -> E3372: + return E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth']) + + def get_modem_data(modem_cfg: dict, get_raw=False) -> Union[dict, tuple]: - cl = E3372(modem_cfg['ip'], legacy_token_auth=modem_cfg['legacy_auth']) + cl = get_modem_client(modem_cfg) signal = cl.device_signal status = cl.monitoring_status @@ -190,6 +197,8 @@ class WebSite(http.HTTPServer): self.get('/inverter.cgi', self.inverter) self.get('/inverter.ajx', self.inverter_ajx) self.get('/pump.cgi', self.pump) + self.get('/sms.cgi', self.sms) + self.post('/sms.cgi', self.sms_post) async def render_page(self, req: http.Request, @@ -208,8 +217,12 @@ class WebSite(http.HTTPServer): return response async def index(self, req: http.Request): + ctx = {} + for k in 'inverter', 'sensors': + ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url'] return await self.render_page(req, 'index', - title="Home web site") + title="Home web site", + context=ctx) async def modems(self, req: http.Request): return await self.render_page(req, 'modems', @@ -218,7 +231,7 @@ class WebSite(http.HTTPServer): async def modems_ajx(self, req: http.Request): modem = req.query.get('id', None) - if modem not in self._modems_config.getkeys(): + if modem not in self._modems_config.keys(): raise ValueError('invalid modem id') modem_cfg = self._modems_config.get(modem) @@ -234,7 +247,7 @@ class WebSite(http.HTTPServer): async def modems_verbose(self, req: http.Request): modem = req.query.get('id', None) - if modem not in self._modems_config.getkeys(): + if modem not in self._modems_config.keys(): raise ValueError('invalid modem id') modem_cfg = self._modems_config.get(modem) @@ -253,6 +266,49 @@ class WebSite(http.HTTPServer): title=f'Подробная информация о модеме "{modem_name}"', context=dict(data=data, modem_name=modem_name)) + async def sms(self, req: http.Request): + modem = req.query.get('id', list(self._modems_config.keys())[0]) + is_outbox = int(req.query.get('outbox', 0)) == 1 + error = req.query.get('error', None) + sent = int(req.query.get('sent', 0)) == 1 + + cl = get_modem_client(self._modems_config[modem]) + messages = cl.sms_list(1, 20, is_outbox) + return await self.render_page(req, 'sms', + title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})", + context=dict( + modems=self._modems_config, + selected_modem=modem, + is_outbox=is_outbox, + error=error, + is_sent=sent, + messages=messages + )) + + async def sms_post(self, req: http.Request): + modem = req.query.get('id', list(self._modems_config.keys())[0]) + is_outbox = int(req.query.get('outbox', 0)) == 1 + + fd = await req.post() + phone = fd.get('phone', None) + text = fd.get('text', None) + + return_url = f'/sms.cgi?id={modem}&outbox={int(is_outbox)}' + phone = re.sub('\s+', '', phone) + + if len(phone) > 4: + country = None + if not phone.startswith('+'): + country = 'RU' + number = phonenumbers.parse(phone, country) + if not phonenumbers.is_valid_number(number): + raise HTTPFound(f'{return_url}&error=Неверный+номер') + phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164) + + cl = get_modem_client(self._modems_config[modem]) + cl.sms_send(phone, text) + raise HTTPFound(return_url) + async def inverter(self, req: http.Request): action = req.query.get('do', None) if action == 'set-osp': diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index eb2ad82..fec92a6 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -78,8 +78,14 @@ class BaseConfigUnit(ABC): raise KeyError(f'option {key} not found') - def getkeys(self): - return list(self._data.keys()) + def values(self): + return self._data.values() + + def keys(self): + return self._data.keys() + + def items(self): + return self._data.items() class ConfigUnit(BaseConfigUnit): diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php index 8179620..94ad75b 100644 --- a/localwebsite/handlers/ModemHandler.php +++ b/localwebsite/handlers/ModemHandler.php @@ -95,85 +95,6 @@ class ModemHandler extends RequestHandler $this->tpl->render_page('routing_dhcp_page.twig'); } - public function GET_sms() { - global $config; - - list($selected, $is_outbox, $error, $sent) = $this->input('modem, b:outbox, error, b:sent'); - if (!$selected) - $selected = array_key_first($config['modems']); - - $cfg = $config['modems'][$selected]; - $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']); - $messages = $e3372->getSMSList(1, 20, $is_outbox); - - $this->tpl->set([ - 'modems_list' => array_keys($config['modems']), - 'modems' => $config['modems'], - 'selected_modem' => $selected, - 'messages' => $messages, - 'is_outbox' => $is_outbox, - 'error' => $error, - 'is_sent' => $sent - ]); - - $direction = $is_outbox ? 'исходящие' : 'входящие'; - $this->tpl->set_title('SMS-сообщения ('.$direction.', '.$selected.')'); - $this->tpl->render_page('sms_page.twig'); - } - - public function POST_sms() { - global $config; - - list($selected, $is_outbox, $phone, $text) = $this->input('modem, b:outbox, phone, text'); - if (!$selected) - $selected = array_key_first($config['modems']); - - $return_url = '/sms/?modem='.$selected; - if ($is_outbox) - $return_url .= '&outbox=1'; - - $go_back = function(?string $error = null) use ($return_url) { - if (!is_null($error)) - $return_url .= '&error='.urlencode($error); - else - $return_url .= '&sent=1'; - redirect($return_url); - }; - - $phone = preg_replace('/\s+/', '', $phone); - - // при отправке смс на короткие номера не надо использовать libphonenumber и вот это вот всё - if (strlen($phone) > 4) { - $country = null; - if (!startsWith($phone, '+')) - $country = 'RU'; - - $phoneUtil = PhoneNumberUtil::getInstance(); - try { - $number = $phoneUtil->parse($phone, $country); - } catch (NumberParseException $e) { - debugError(__METHOD__.': failed to parse number '.$phone.': '.$e->getMessage()); - $go_back('Неверный номер ('.$e->getMessage().')'); - return; - } - - if (!$phoneUtil->isValidNumber($number)) { - $go_back('Неверный номер'); - return; - } - - $phone = $phoneUtil->format($number, PhoneNumberFormat::E164); - } - - $cfg = $config['modems'][$selected]; - $e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']); - - $result = $e3372->sendSMS($phone, $text); - debugLog($result); - - $go_back(); - } - protected static function getCurrentUpstream() { global $config; diff --git a/requirements.txt b/requirements.txt index 8fa67c3..c242f38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ psutil~=5.9.1 aioshutil~=1.1 scikit-image==0.21.0 cerberus~=1.3.4 +phonenumbers~=8.13.28 # following can be installed from debian repositories # matplotlib~=3.5.0 diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2 index e3ab421..c356326 100644 --- a/web/kbn_templates/index.j2 +++ b/web/kbn_templates/index.j2 @@ -23,9 +23,9 @@
    Другое
    Все камеры (HQ)
    diff --git a/localwebsite/templates-web/sms_page.twig b/web/kbn_templates/sms.j2 similarity index 73% rename from localwebsite/templates-web/sms_page.twig rename to web/kbn_templates/sms.j2 index 112fa64..6de9d42 100644 --- a/localwebsite/templates-web/sms_page.twig +++ b/web/kbn_templates/sms.j2 @@ -1,14 +1,13 @@ -{% include 'bc.twig' with { - history: [ - {text: "SMS-сообщения" } - ] -} %} +{% extends "base.j2" %} + +{% block content %} +{{ breadcrumbs([{'text': 'SMS-сообщения'}]) }}