├── .gitattributes ├── .gitignore ├── BMSStats.json ├── Code ├── ESP32 │ ├── ESP_Workspace.code-workspace │ ├── PylonToMQTT │ │ ├── .gitignore │ │ ├── .vscode │ │ │ └── extensions.json │ │ ├── include │ │ │ ├── AsyncSerial.h │ │ │ ├── AsyncSerialCallbackInterface.h │ │ │ ├── Defines.h │ │ │ ├── Enumerations.h │ │ │ ├── HelperFunctions.h │ │ │ ├── IOT.h │ │ │ ├── IOTCallbackInterface.h │ │ │ ├── IOTServiceInterface.h │ │ │ ├── Log.h │ │ │ ├── Pack.h │ │ │ ├── Pylon.h │ │ │ ├── README │ │ │ ├── WebLog.h │ │ │ └── html.h │ │ ├── platformio.ini │ │ └── src │ │ │ ├── AsyncSerial.cpp │ │ │ ├── IOT.cpp │ │ │ ├── Pack.cpp │ │ │ ├── Pylon.cpp │ │ │ ├── WebLog.cpp │ │ │ └── main.cpp │ └── README.md ├── Python │ ├── .gitignore │ ├── Dockerfile │ ├── IOTStack_docker-compose.yml │ ├── LICENSE.md │ ├── README.md │ ├── compose-override.yml │ ├── docker-compose.yml │ ├── pylon_to_mqtt.py │ ├── support │ │ ├── __init__.py │ │ ├── pylon_jsonencoder.py │ │ ├── pylon_validate.py │ │ └── pylontech.py │ └── telegraf.conf └── node-red │ ├── P16100A-Simulator.json │ ├── flows.json │ └── notes.txt ├── Docs ├── GetAlarmInfo.txt ├── GetAnalogValue.txt ├── GetBarCode.txt ├── GetBarCodes_Trace.log ├── GetBarCodes_Trace.txt ├── GetParameterSettings.txt ├── GetVersionInfo.txt ├── PACE 16S 100Ah.pdf ├── SampleMQTT.json ├── Traces.txt └── docker hub notes.txt ├── Jakiper Battery.json ├── New dashboard-1667136343536.json ├── Pictures ├── BMSStats.png ├── DCE RS232 Adapter.PNG ├── Flasher1.PNG ├── Flasher3.PNG ├── Flasher5.PNG ├── Flasher6.PNG ├── Flasher7.PNG ├── IOTStack_BuildStack.PNG ├── IOTStack_Containers.PNG ├── IOTStack_MQTTLogs.PNG ├── IOTStack_MQTTfxSetup.PNG ├── IOTStack_MQTTfxSubscribe.PNG ├── IOTStack_docker_ps.PNG ├── Jakiper RJ11.jpg ├── Jakiper.png ├── PylonToMQTT.png ├── RS-232_DE-9_Connector_Pinouts.png ├── RS232_Wiring.png ├── WIP.jpg └── grafana.PNG └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /Code/ESP32/PylonToMQTT/.vscode 3 | -------------------------------------------------------------------------------- /Code/ESP32/ESP_Workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "PylonToMQTT" 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "array": "cpp", 10 | "string": "cpp", 11 | "string_view": "cpp", 12 | "cstddef": "cpp", 13 | "atomic": "cpp", 14 | "*.tcc": "cpp", 15 | "bitset": "cpp", 16 | "chrono": "cpp", 17 | "deque": "cpp", 18 | "list": "cpp", 19 | "unordered_map": "cpp", 20 | "unordered_set": "cpp", 21 | "vector": "cpp", 22 | "iterator": "cpp", 23 | "memory_resource": "cpp", 24 | "optional": "cpp", 25 | "fstream": "cpp", 26 | "istream": "cpp", 27 | "ostream": "cpp", 28 | "sstream": "cpp", 29 | "streambuf": "cpp", 30 | "system_error": "cpp", 31 | "thread": "cpp", 32 | "functional": "cpp", 33 | "regex": "cpp", 34 | "tuple": "cpp", 35 | "cctype": "cpp", 36 | "cinttypes": "cpp", 37 | "clocale": "cpp", 38 | "cmath": "cpp", 39 | "condition_variable": "cpp", 40 | "csignal": "cpp", 41 | "cstdarg": "cpp", 42 | "cstdint": "cpp", 43 | "cstdio": "cpp", 44 | "cstdlib": "cpp", 45 | "cstring": "cpp", 46 | "ctime": "cpp", 47 | "cwchar": "cpp", 48 | "cwctype": "cpp", 49 | "exception": "cpp", 50 | "algorithm": "cpp", 51 | "map": "cpp", 52 | "memory": "cpp", 53 | "numeric": "cpp", 54 | "random": "cpp", 55 | "ratio": "cpp", 56 | "type_traits": "cpp", 57 | "utility": "cpp", 58 | "initializer_list": "cpp", 59 | "iomanip": "cpp", 60 | "iosfwd": "cpp", 61 | "limits": "cpp", 62 | "mutex": "cpp", 63 | "new": "cpp", 64 | "stdexcept": "cpp", 65 | "typeinfo": "cpp", 66 | "iostream": "cpp" 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/AsyncSerial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | #include "Enumerations.h" 4 | #include "AsyncSerialCallbackInterface.h" 5 | 6 | namespace PylonToMQTT 7 | { 8 | 9 | class AsyncSerial 10 | { 11 | public: 12 | AsyncSerial(); 13 | ~AsyncSerial(); 14 | void begin(AsyncSerialCallbackInterface* cbi, unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin); 15 | void Receive(int timeOut); 16 | void Send(CommandInformation cmd, byte* data, size_t dataLength); 17 | byte* GetContent(); 18 | uint16_t GetContentLength(); 19 | CommandInformation GetToken() { return _command; }; 20 | 21 | unsigned long Timeout = 0; 22 | char EOIChar = '\r'; 23 | char SOIChar = '~'; 24 | 25 | protected: 26 | inline bool IsExpired(); 27 | Stream* _stream; 28 | byte *_buffer; 29 | size_t _bufferIndex; 30 | size_t _bufferLength; 31 | unsigned long _startTime; 32 | Status _status; 33 | CommandInformation _command; 34 | AsyncSerialCallbackInterface* _cbi; 35 | }; 36 | 37 | } // namespace PylonToMQTT 38 | 39 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/AsyncSerialCallbackInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class AsyncSerialCallbackInterface 5 | { 6 | public: 7 | virtual void complete() = 0; 8 | virtual void overflow() = 0; 9 | virtual void timeout() = 0; 10 | }; -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/Defines.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #define TAG "PylonToMQTT" 5 | 6 | #define WATCHDOG_TIMER 600000 //time in ms to trigger the watchdog 7 | #define COMMAND_PUBLISH_RATE 500 // delay between sequence of pylon commands sent to battery 8 | #define SERIAL_RECEIVE_TIMEOUT 3000 // time in ms to wait for serial data from battery 9 | 10 | #define STR_LEN 255 // general string buffer size 11 | #define CONFIG_LEN 32 // configuration string buffer size 12 | #define NUMBER_CONFIG_LEN 6 13 | #define DEFAULT_AP_PASSWORD "12345678" 14 | 15 | #define MAX_PUBLISH_RATE 30000 16 | #define MIN_PUBLISH_RATE 1000 17 | #define CheckBit(var,pos) ((var) & (1<<(pos))) ? true : false 18 | #define toShort(i, v) (v[i++]<<8) | v[i++] 19 | 20 | #define TempKeys std::string _tempKeys[] = { "CellTemp1_4", "CellTemp5_8", "CellTemp9_12", "CellTemp13_16", "MOS_T", "ENV_T"}; 21 | 22 | #define ASYNC_WEBSERVER_PORT 7667 23 | #define IOTCONFIG_PORT 80 24 | #define WSOCKET_LOG_PORT 7668 25 | #define WSOCKET_HOME_PORT 7669 26 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/Enumerations.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // struct JakiperInfo 6 | // { 7 | // float BatCurrent = 0; 8 | // float BatVoltage = 0; 9 | // uint16_t SOC = 0; 10 | // uint16_t SOH = 0; 11 | // float RemainingCapacity = 0; 12 | // float FullCapacity = 0; 13 | // uint16_t CycleCount = 0; 14 | // float Cell1 = 0; 15 | // float Cell2 = 0; 16 | // float Cell3 = 0; 17 | // float Cell4 = 0; 18 | // float Cell5 = 0; 19 | // float Cell6 = 0; 20 | // float Cell7 = 0; 21 | // float Cell8 = 0; 22 | // float Cell9 = 0; 23 | // float Cell10 = 0; 24 | // float Cell11 = 0; 25 | // float Cell12 = 0; 26 | // float Cell13 = 0; 27 | // float Cell14 = 0; 28 | // float Cell15 = 0; 29 | // float Cell16 = 0; 30 | // float Cell1Temp = 0; 31 | // float Cell2Temp = 0; 32 | // float Cell3Temp = 0; 33 | // float Cell4Temp = 0; 34 | // float MosTemp = 0; 35 | // float EnvTemp = 0; 36 | 37 | // }; 38 | 39 | 40 | namespace PylonToMQTT 41 | { 42 | 43 | typedef enum 44 | { 45 | IDDLE, 46 | RECEIVING_DATA, 47 | MESSAGE_RECEIVED, 48 | DATA_OVERFLOW, 49 | TIMEOUT, 50 | } Status; 51 | 52 | enum CommandInformation : byte 53 | { 54 | None = 0x00, 55 | AnalogValueFixedPoint = 0x42, 56 | AlarmInfo = 0x44, 57 | SystemParameterFixedPoint = 0x47, 58 | ProtocolVersion = 0x4F, 59 | ManufacturerInfo = 0x51, 60 | GetPackCount = 0x90, 61 | GetChargeDischargeManagementInfo = 0x92, 62 | Serialnumber = 0x93, 63 | FirmwareInfo = 0x96, 64 | RemainingCapacity = 0xA6, 65 | BMSTime = 0xB1, 66 | GetVersionInfo = 0xC1, 67 | GetBarCode = 0xC2, 68 | GetCellOV = 0xD1, 69 | StartCurrent = 0xED, 70 | }; 71 | 72 | enum ResponseCode 73 | { 74 | Normal = 0x00, 75 | VER_error = 0x01, 76 | CHKSUM_error = 0x02, 77 | LCHKSUM_error = 0x03, 78 | CID2invalid = 0x04, 79 | CommandFormat_error = 0x05, 80 | InvalidData = 0x06, 81 | ADR_error = 0x90, 82 | CID2Communicationinvalid_error = 0x91 83 | }; 84 | 85 | } // namespace PylonToMQTT -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/HelperFunctions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "Defines.h" 5 | 6 | boolean inline requiredParam(iotwebconf::WebRequestWrapper* webRequestWrapper, iotwebconf::InputParameter& param) { 7 | boolean valid = true; 8 | int paramLength = webRequestWrapper->arg(param.getId()).length(); 9 | if (paramLength == 0) 10 | { 11 | param.errorMessage = "This field is required\n"; 12 | valid = false; 13 | } 14 | return valid; 15 | } 16 | 17 | std::string inline formatDuration(unsigned long milliseconds) { 18 | const unsigned long MS_PER_SECOND = 1000; 19 | const unsigned long MS_PER_MINUTE = MS_PER_SECOND * 60; 20 | const unsigned long MS_PER_HOUR = MS_PER_MINUTE * 60; 21 | const unsigned long MS_PER_DAY = MS_PER_HOUR * 24; 22 | 23 | unsigned long days = milliseconds / MS_PER_DAY; 24 | milliseconds %= MS_PER_DAY; 25 | unsigned long hours = milliseconds / MS_PER_HOUR; 26 | milliseconds %= MS_PER_HOUR; 27 | unsigned long minutes = milliseconds / MS_PER_MINUTE; 28 | milliseconds %= MS_PER_MINUTE; 29 | unsigned long seconds = milliseconds / MS_PER_SECOND; 30 | 31 | std::string result = std::to_string(days) + " days, " + 32 | std::to_string(hours) + " hours, " + 33 | std::to_string(minutes) + " minutes, " + 34 | std::to_string(seconds) + " seconds"; 35 | 36 | return result; 37 | } 38 | 39 | unsigned long inline getTime() { 40 | time_t now; 41 | struct tm timeinfo; 42 | if (!getLocalTime(&timeinfo)) { 43 | return(0); 44 | } 45 | time(&now); 46 | return now; 47 | } 48 | 49 | template String htmlConfigEntry(const char* label, T val) 50 | { 51 | String s = "
  • "; 52 | s += label; 53 | s += ": "; 54 | s += val; 55 | s += "
  • "; 56 | return s; 57 | } 58 | 59 | void inline light_sleep(uint32_t sec ) 60 | { 61 | esp_sleep_enable_timer_wakeup(sec * 1000000ULL); 62 | esp_light_sleep_start(); 63 | } -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/IOT.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "WiFi.h" 4 | #include "ArduinoJson.h" 5 | #include 6 | extern "C" 7 | { 8 | #include "freertos/FreeRTOS.h" 9 | #include "freertos/timers.h" 10 | } 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include "Defines.h" 20 | #include "IOTServiceInterface.h" 21 | #include "IOTCallbackInterface.h" 22 | 23 | namespace PylonToMQTT 24 | { 25 | class IOT : public IOTServiceInterface 26 | { 27 | public: 28 | IOT() {}; 29 | void Init(IOTCallbackInterface *iotCB); 30 | 31 | boolean Run(); 32 | boolean Publish(const char *subtopic, const char *value, boolean retained = false); 33 | boolean Publish(const char *subtopic, JsonDocument &payload, boolean retained = false); 34 | boolean Publish(const char *subtopic, float value, boolean retained = false); 35 | boolean PublishMessage(const char *topic, JsonDocument &payload, boolean retained); 36 | boolean PublishHADiscovery(const char *bank, JsonDocument &payload); 37 | std::string getRootTopicPrefix(); 38 | std::string getSubtopicName(); 39 | u_int getUniqueId() { return _uniqueId; }; 40 | std::string getThingName(); 41 | void Online(); 42 | IOTCallbackInterface *IOTCB() { return _iotCB; } 43 | unsigned long PublishRate(); 44 | 45 | private: 46 | bool _clientsConfigured = false; 47 | IOTCallbackInterface *_iotCB; 48 | u_int _uniqueId = 0; // unique id from mac address NIC segment 49 | bool _publishedOnline = false; 50 | hw_timer_t *_watchdogTimer = NULL; 51 | }; 52 | 53 | const char reboot_html[] PROGMEM = R"rawliteral( 54 | 55 | ESP32 Reboot 56 | 57 |

    Rebooting ESP32

    58 |

    Return to Settings after reboot has completed.

    59 | 60 | )rawliteral"; 61 | 62 | const char redirect_html[] PROGMEM = R"rawliteral( 63 | 64 | Redirecting... 65 | 66 | 67 |

    5 | #include 6 | 7 | class IOTCallbackInterface 8 | { 9 | public: 10 | virtual String getSettingsHTML() = 0; 11 | virtual iotwebconf::ParameterGroup* parameterGroup() = 0; 12 | virtual bool validate(iotwebconf::WebRequestWrapper* webRequestWrapper) = 0; 13 | virtual void onMqttConnect(bool sessionPresent) = 0; 14 | virtual void onMqttMessage(char* topic, JsonDocument& doc) = 0; 15 | virtual void onWiFiConnect() = 0; 16 | }; -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/IOTServiceInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | #include "ArduinoJson.h" 4 | 5 | 6 | class IOTServiceInterface 7 | { 8 | public: 9 | 10 | virtual boolean Publish(const char *subtopic, const char *value, boolean retained) = 0; 11 | virtual boolean Publish(const char *subtopic, float value, boolean retained) = 0; 12 | virtual boolean PublishMessage(const char* topic, JsonDocument& payload, boolean retained) = 0; 13 | virtual boolean PublishHADiscovery(const char *bank, JsonDocument& payload) = 0; 14 | virtual std::string getRootTopicPrefix() = 0; 15 | virtual std::string getSubtopicName() = 0; 16 | virtual u_int getUniqueId() = 0; 17 | virtual std::string getThingName() = 0; 18 | virtual void Online() = 0; 19 | }; -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "esp_log.h" 5 | #include 6 | 7 | int weblog(const char *format, ...); 8 | 9 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_VERBOSE 10 | #define logv(format, ...) weblog(ARDUHAL_LOG_FORMAT(V, format), ##__VA_ARGS__) 11 | #else 12 | #define logv(format, ...) 13 | #endif 14 | 15 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG 16 | #define logd(format, ...) weblog(ARDUHAL_LOG_FORMAT(D, format), ##__VA_ARGS__) 17 | #else 18 | #define logd(format, ...) 19 | #endif 20 | 21 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO 22 | #define logi(format, ...) weblog(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__) 23 | #else 24 | #define logi(format, ...) 25 | #endif 26 | 27 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_WARN 28 | #define logw(format, ...) weblog(ARDUHAL_LOG_FORMAT(W, format), ##__VA_ARGS__) 29 | #else 30 | #define logw(format, ...) 31 | #endif 32 | 33 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_ERROR 34 | #define loge(format, ...) weblog(ARDUHAL_LOG_FORMAT(E, format), ##__VA_ARGS__) 35 | #else 36 | #define loge(format, ...) 37 | #endif 38 | 39 | void inline printLocalTime() 40 | { 41 | #if APP_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG 42 | struct tm timeinfo; 43 | if (!getLocalTime(&timeinfo)) 44 | { 45 | logi("Failed to obtain time"); 46 | return; 47 | } 48 | char buf[64]; 49 | buf[0] = 0; 50 | strftime(buf, 64, "%A, %B %d %Y %H:%M:%S", &timeinfo); 51 | logi("Date Time: %s", buf); 52 | #endif 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/Pack.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Arduino.h" 3 | #include "ArduinoJson.h" 4 | #include 5 | #include "IOTCallbackInterface.h" 6 | #include "IOTServiceInterface.h" 7 | 8 | using namespace std; 9 | 10 | namespace PylonToMQTT 11 | { 12 | class Pack { 13 | public: 14 | Pack(const std::string& name, std::vector* tempKeys, IOTServiceInterface* pcb) { _name = name; _pTempKeys = tempKeys; _psi = pcb; }; 15 | 16 | std::string Name() { 17 | return _name; 18 | } 19 | std::string getBarcode() { 20 | return _barCode; 21 | } 22 | 23 | void setBarcode(const std::string& bc) { 24 | _barCode = bc; 25 | } 26 | 27 | void setVersionInfo(const std::string& ver) { 28 | _versionInfo = ver; 29 | } 30 | 31 | void setNumberOfCells(int val) { 32 | _numberOfCells = val; 33 | } 34 | 35 | void setNumberOfTemps(int val) { 36 | _numberOfTemps = val; 37 | } 38 | 39 | void PublishDiscovery(); 40 | 41 | bool InfoPublished() { 42 | return _infoPublised; 43 | } 44 | void SetInfoPublished() { 45 | _infoPublised = true; 46 | } 47 | 48 | protected: 49 | bool ReadyToPublish() { 50 | return (!_discoveryPublished && InfoPublished() && _numberOfTemps > 0 && _numberOfCells > 0); 51 | } 52 | 53 | 54 | private: 55 | std::string _name; 56 | std::string _barCode; 57 | std::string _versionInfo; 58 | boolean _infoPublised = false; 59 | boolean _discoveryPublished = false; 60 | IOTServiceInterface* _psi; 61 | std::vector* _pTempKeys; 62 | int _numberOfCells = 0; 63 | int _numberOfTemps = 0; 64 | }; 65 | } -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/Pylon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "IOTServiceInterface.h" 5 | #include "AsyncSerial.h" 6 | #include "Pack.h" 7 | #include "Defines.h" 8 | 9 | namespace PylonToMQTT 10 | { 11 | 12 | class Pylon : public AsyncSerialCallbackInterface, public IOTCallbackInterface 13 | { 14 | 15 | public: 16 | Pylon(); 17 | ~Pylon(); 18 | void begin(IOTServiceInterface *pcb) 19 | { 20 | _psi = pcb; 21 | _asyncSerial->begin(this, BAUDRATE, SERIAL_8N1, RXPIN, TXPIN); 22 | }; 23 | void Process(); 24 | void Receive(int timeOut) { _asyncSerial->Receive(timeOut); }; 25 | bool Transmit(); 26 | int ParseResponse(char *szResponse, size_t readNow, CommandInformation cmd); 27 | 28 | // IOTCallbackInterface 29 | String getSettingsHTML(); 30 | iotwebconf::ParameterGroup *parameterGroup(); 31 | bool validate(iotwebconf::WebRequestWrapper *webRequestWrapper); 32 | void onMqttConnect(bool sessionPresent); 33 | void onMqttMessage(char* topic, JsonDocument& doc); 34 | void onWiFiConnect(); 35 | 36 | // AsyncSerialCallbackInterface 37 | void complete() 38 | { 39 | ParseResponse((char *)_asyncSerial->GetContent(), _asyncSerial->GetContentLength(), _asyncSerial->GetToken()); 40 | }; 41 | void overflow() 42 | { 43 | loge("AsyncSerial: overflow"); 44 | }; 45 | void timeout() 46 | { 47 | loge("AsyncSerial: timeout"); 48 | }; 49 | 50 | protected: 51 | JsonDocument _root; 52 | uint8_t _infoCommandIndex = 0; 53 | uint8_t _readingsCommandIndex = 0; 54 | uint8_t _numberOfPacks = 0; 55 | uint8_t _currentPack = 0; 56 | AsyncSerial *_asyncSerial; 57 | IOTServiceInterface *_psi; 58 | CommandInformation _currentCommand = CommandInformation::None; 59 | 60 | uint16_t get_frame_checksum(char *frame); 61 | int get_info_length(const char *info); 62 | void encode_cmd(char *frame, uint8_t address, uint8_t cid2, const char *info); 63 | String convert_ASCII(char *p); 64 | int parseValue(char **pp, int l); 65 | void send_cmd(uint8_t address, CommandInformation cmd); 66 | 67 | private: 68 | std::vector _Packs; 69 | std::vector _TempKeys; 70 | }; 71 | } // namespace PylonToMQTT 72 | 73 | 74 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/WebLog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "esp_log.h" 5 | #include 6 | #include "defines.h" 7 | #include 8 | #include 9 | 10 | // Store HTML content with JavaScript to receive serial log data via WebSocket 11 | const char web_serial_html[] PROGMEM = R"rawliteral( 12 | 13 | 14 | 15 | ESP32 Serial Log 16 | 38 | 39 | 40 |

    ESP32 Serial Log

    41 |
    Connecting to WebSocket...

    42 |
    43 | 44 | 45 | )rawliteral"; 46 | 47 | class WebLog 48 | { 49 | public: 50 | WebLog() {}; 51 | void begin(AsyncWebServer *pwebServer); 52 | void process(); 53 | 54 | private: 55 | }; 56 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/include/html.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | const char home_html[] PROGMEM = R"rawliteral( 6 | 7 | 8 | {n} 9 | 10 | 11 | 12 | 13 |

    {n}

    14 |
    Firmware config version '{v}'
    15 |
    16 | 17 |

    18 |

    22 | )rawliteral"; -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32@^6.10.0 13 | board = esp32dev 14 | framework = arduino 15 | monitor_speed = 115200 16 | # upload_port = COM8 17 | ; monitor_port = COM3 18 | ; monitor_dtr = 0 19 | ; monitor_rts = 0 20 | 21 | lib_deps = 22 | bblanchon/ArduinoJson @ ^7.3.0 23 | prampec/IotWebConf@^3.2.1 24 | marvinroger/AsyncMqttClient@^0.9.0 25 | ESP32Async/ESPAsyncWebServer@ 3.6.0 26 | ESP32Async/AsyncTCP @ 3.3.2 27 | links2004/WebSockets @ ^2.6.1 28 | 29 | build_flags = 30 | 31 | -D 'CONFIG_VERSION="V2.0.1"' ; major.minor.build (major or minor will invalidate the configuration) 32 | -D 'NTP_SERVER="pool.ntp.org"' 33 | -D 'HOME_ASSISTANT_PREFIX="homeassistant"' ; Home Assistant Auto discovery root topic 34 | 35 | -D BAUDRATE=9600 # Pylon console baud rate 36 | -D RXPIN=GPIO_NUM_16 37 | -D TXPIN=GPIO_NUM_17 38 | 39 | -D WIFI_STATUS_PIN=2 ;LED Pin on the ESP32 dev module, indicates AP mode when flashing 40 | -D FACTORY_RESET_PIN=4 ; Clear NVRAM 41 | 42 | ; logs 43 | ; -D APP_LOG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG 44 | -D APP_LOG_LEVEL=ARDUHAL_LOG_LEVEL_INFO 45 | 46 | -D IOTWEBCONF_DEBUG_TO_SERIAL 47 | -D IOTWEBCONF_DEBUG_PWD_TO_SERIAL 48 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/AsyncSerial.cpp: -------------------------------------------------------------------------------- 1 | #include "AsyncSerial.h" 2 | #include "Log.h" 3 | 4 | #define BufferSize 2048 5 | 6 | namespace PylonToMQTT 7 | { 8 | 9 | AsyncSerial::AsyncSerial() 10 | { 11 | _status = IDDLE; 12 | _buffer = (byte*)malloc(BufferSize); 13 | _bufferLength = BufferSize; 14 | _bufferIndex = 0; 15 | } 16 | 17 | AsyncSerial::~AsyncSerial() 18 | { 19 | free(_buffer); 20 | } 21 | 22 | void AsyncSerial::begin(AsyncSerialCallbackInterface* cbi, unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin) 23 | { 24 | _cbi = cbi; 25 | Serial2.begin(baud, config, rxPin, txPin); 26 | while (!Serial2) {} 27 | _stream = &Serial2; 28 | } 29 | 30 | void AsyncSerial::Receive(int timeOut) 31 | { 32 | if (_status != RECEIVING_DATA) { return; } 33 | Timeout = timeOut; 34 | _startTime = millis(); 35 | bool SOIfound = false; 36 | while (_status < MESSAGE_RECEIVED) 37 | { 38 | if (IsExpired()) 39 | { 40 | _status = TIMEOUT; 41 | if (_cbi != nullptr) _cbi->timeout(); 42 | break; 43 | } 44 | if (_status == RECEIVING_DATA) 45 | { 46 | while (_stream->available()) 47 | { 48 | byte newData = _stream->read(); 49 | if (SOIfound) { 50 | if (newData == (byte)EOIChar) { 51 | _status = MESSAGE_RECEIVED; 52 | _buffer[_bufferIndex] = 0; 53 | if (_cbi != nullptr) _cbi->complete(); // call service function to handle payload 54 | break; 55 | } 56 | else { 57 | if (_bufferIndex >= _bufferLength) { 58 | _status = DATA_OVERFLOW; 59 | if (_cbi != nullptr) _cbi->overflow(); 60 | break; 61 | } 62 | else { 63 | _buffer[_bufferIndex++] = newData; 64 | } 65 | } 66 | } 67 | else if (newData == (byte)SOIChar) { // discard until SOI received 68 | _bufferIndex = 0; 69 | memset(_buffer, 0, BufferSize); // clear buffer on new SOI 70 | _buffer[_bufferIndex++] = newData; 71 | SOIfound = true; 72 | } 73 | } 74 | } 75 | } 76 | _bufferIndex = 0; // recieved, timedout or overflowed - reset for next message 77 | _status = IDDLE; 78 | return; 79 | } 80 | 81 | void AsyncSerial::Send(CommandInformation cmd, byte* data, size_t dataLength) 82 | { 83 | if (_status != IDDLE) { loge("Not Idle!"); return; } 84 | _stream->write(data, dataLength); 85 | _status = RECEIVING_DATA; 86 | _command = cmd; 87 | return; 88 | } 89 | 90 | inline bool AsyncSerial::IsExpired() 91 | { 92 | if (Timeout == 0) return false; 93 | return ((unsigned long)(millis() - _startTime) > Timeout); 94 | } 95 | 96 | byte * AsyncSerial::GetContent() 97 | { 98 | return _buffer; 99 | } 100 | 101 | uint16_t AsyncSerial::GetContentLength() 102 | { 103 | return _bufferIndex; 104 | } 105 | 106 | } // namespace PylonToMQTT -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/IOT.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include "time.h" 5 | #include "Log.h" 6 | #include "HelperFunctions.h" 7 | #include "IOT.h" 8 | #include "IotWebConfOptionalGroup.h" 9 | #include 10 | 11 | namespace PylonToMQTT 12 | { 13 | 14 | AsyncMqttClient _mqttClient; 15 | TimerHandle_t mqttReconnectTimer; 16 | DNSServer _dnsServer; 17 | HTTPUpdateServer _httpUpdater; 18 | WebServer webServer(IOTCONFIG_PORT); 19 | IotWebConf _iotWebConf(TAG, &_dnsServer, &webServer, DEFAULT_AP_PASSWORD, CONFIG_VERSION); 20 | unsigned long _lastBootTimeStamp = millis(); 21 | char _willTopic[STR_LEN]; 22 | char _rootTopicPrefix[64]; 23 | IotWebConfParameterGroup mqttGroup = IotWebConfParameterGroup("mqtt", "MQTT configuration"); 24 | iotwebconf::TextTParameter mqttServerParam = iotwebconf::Builder>("mqttServer").label("MQTT server").defaultValue("").build(); 25 | iotwebconf::IntTParameter mqttPortParam = iotwebconf::Builder>("mqttPort").label("MQTT port").defaultValue(1883).build(); 26 | iotwebconf::TextTParameter mqttUserNameParam = iotwebconf::Builder>("mqttUserName").label("MQTT user").defaultValue("").build(); 27 | iotwebconf::PasswordTParameter mqttUserPasswordParam = iotwebconf::Builder>("mqttUserPassword").label("MQTT password").defaultValue("").build(); 28 | iotwebconf::TextTParameter mqttSubtopicParam = iotwebconf::Builder>("bankName").label("Battery Bank Name").defaultValue("Bank1").build(); 29 | iotwebconf::IntTParameter publishRateParam = iotwebconf::Builder>("publishRateStr").label("Publish Rate (S)").defaultValue(2).min(1).max(30).build(); 30 | 31 | void IOT::Init(IOTCallbackInterface *iotCB) 32 | { 33 | _iotCB = iotCB; 34 | pinMode(FACTORY_RESET_PIN, INPUT_PULLUP); 35 | _iotWebConf.setStatusPin(WIFI_STATUS_PIN); 36 | 37 | // setup EEPROM parameters 38 | mqttGroup.addItem(&mqttServerParam); 39 | mqttGroup.addItem(&mqttPortParam); 40 | mqttGroup.addItem(&mqttUserNameParam); 41 | mqttGroup.addItem(&mqttUserPasswordParam); 42 | mqttGroup.addItem(&mqttSubtopicParam); 43 | mqttGroup.addItem(&publishRateParam); 44 | 45 | _iotWebConf.addSystemParameter(&mqttGroup); 46 | if (_iotCB->parameterGroup() != NULL) 47 | { 48 | _iotWebConf.addParameterGroup(_iotCB->parameterGroup()); 49 | } 50 | _iotWebConf.getApTimeoutParameter()->visible = true; 51 | 52 | // setup callbacks for IotWebConf 53 | _iotWebConf.setConfigSavedCallback([this]() { 54 | logi("Configuration was updated."); 55 | }); 56 | _iotWebConf.setFormValidator([this](iotwebconf::WebRequestWrapper *webRequestWrapper) { 57 | if (IOTCB()->validate(webRequestWrapper) == false) 58 | return false; 59 | return true; 60 | }); 61 | _iotWebConf.setupUpdateServer( 62 | [](const char *updatePath) 63 | { _httpUpdater.setup(&webServer, updatePath); }, 64 | [](const char *userName, char *password) 65 | { _httpUpdater.updateCredentials(userName, password); }); 66 | 67 | if (digitalRead(FACTORY_RESET_PIN) == LOW) 68 | { 69 | EEPROM.begin(IOTWEBCONF_CONFIG_START + IOTWEBCONF_CONFIG_VERSION_LENGTH); 70 | for (byte t = 0; t < IOTWEBCONF_CONFIG_VERSION_LENGTH; t++) 71 | { 72 | EEPROM.write(IOTWEBCONF_CONFIG_START + t, 0); 73 | } 74 | EEPROM.commit(); 75 | EEPROM.end(); 76 | _iotWebConf.resetWifiAuthInfo(); 77 | logw("Factory Reset!"); 78 | } 79 | mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(5000), pdFALSE, (void *)0, reinterpret_cast(+[] (TimerHandle_t) { 80 | if (WiFi.isConnected()) 81 | { 82 | if (strlen(mqttServerParam.value()) > 0) // mqtt configured 83 | { 84 | logd("Connecting to MQTT..."); 85 | _mqttClient.connect(); 86 | } 87 | } 88 | })); 89 | WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { 90 | logd("[WiFi-event] event: %d", event); 91 | String s; 92 | JsonDocument doc; 93 | switch (event) 94 | { 95 | case SYSTEM_EVENT_STA_GOT_IP: 96 | // logd("WiFi connected, IP address: %s", WiFi.localIP().toString().c_str()); 97 | doc["IP"] = WiFi.localIP().toString().c_str(); 98 | doc["ApPassword"] = DEFAULT_AP_PASSWORD; 99 | serializeJson(doc, s); 100 | s += '\n'; 101 | Serial.printf(s.c_str()); // send json to flash tool 102 | configTime(0, 0, NTP_SERVER); 103 | printLocalTime(); 104 | xTimerStart(mqttReconnectTimer, 0); 105 | this->IOTCB()->onWiFiConnect(); 106 | break; 107 | case SYSTEM_EVENT_STA_DISCONNECTED: 108 | logw("WiFi lost connection"); 109 | xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi 110 | break; 111 | default: 112 | break; 113 | } 114 | }); 115 | boolean validConfig = _iotWebConf.init(); 116 | if (!validConfig) 117 | { 118 | logw("!invalid configuration!"); 119 | _iotWebConf.resetWifiAuthInfo(); 120 | _iotWebConf.getRootParameterGroup()->applyDefaultValue(); 121 | } 122 | else 123 | { 124 | logi("wait in AP mode for %d seconds", _iotWebConf.getApTimeoutMs() / 1000); 125 | if (mqttServerParam.value()[0] != '\0') // skip if factory reset 126 | { 127 | logd("Valid configuration!"); 128 | _clientsConfigured = true; 129 | // setup MQTT 130 | _mqttClient.onConnect( [this](bool sessionPresent) { 131 | logi("Connected to MQTT. Session present: %d", sessionPresent); 132 | char buf[64]; 133 | sprintf(buf, "%s/cmnd/#", _rootTopicPrefix); 134 | _mqttClient.subscribe(buf, 0); 135 | IOTCB()->onMqttConnect(sessionPresent); 136 | _mqttClient.publish(_willTopic, 0, true, "Offline"); // toggle online in run loop 137 | }); 138 | _mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { 139 | logw("Disconnected from MQTT. Reason: %d", (int8_t)reason); 140 | if (WiFi.isConnected()) 141 | { 142 | xTimerStart(mqttReconnectTimer, 5000); 143 | } 144 | }); 145 | _mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { 146 | logd("MQTT Message arrived [%s] qos: %d len: %d index: %d total: %d", topic, properties.qos, len, index, total); 147 | JsonDocument doc; 148 | DeserializationError err = deserializeJson(doc, payload); 149 | if (err) // not json! 150 | { 151 | logd("MQTT payload {%s} is not valid JSON!", payload); 152 | } 153 | else 154 | { 155 | if (doc.containsKey("status")) 156 | { 157 | doc.clear(); 158 | doc["name"] = mqttSubtopicParam.value(); 159 | doc["sw_version"] = CONFIG_VERSION; 160 | doc["IP"] = WiFi.localIP().toString().c_str(); 161 | doc["SSID"] = WiFi.SSID(); 162 | doc["uptime"] = formatDuration(millis() - _lastBootTimeStamp); 163 | Publish("status", doc, true); 164 | } 165 | else 166 | { 167 | IOTCB()->onMqttMessage(topic, doc); 168 | } 169 | } 170 | }); 171 | _mqttClient.onPublish([this](uint16_t packetId) { logd("Publish acknowledged. packetId: %d", packetId); }); 172 | IPAddress ip; 173 | if (ip.fromString(mqttServerParam.value())) 174 | { 175 | _mqttClient.setServer(ip, mqttPortParam.value()); 176 | } 177 | else 178 | { 179 | _mqttClient.setServer(mqttServerParam.value(), mqttPortParam.value()); 180 | } 181 | _mqttClient.setCredentials(mqttUserNameParam.value(), mqttUserPasswordParam.value()); 182 | int len = strlen(_iotWebConf.getThingName()); 183 | strncpy(_rootTopicPrefix, _iotWebConf.getThingName(), len); 184 | if (_rootTopicPrefix[len - 1] != '/') 185 | { 186 | strcat(_rootTopicPrefix, "/"); 187 | } 188 | strcat(_rootTopicPrefix, mqttSubtopicParam.value()); 189 | logd("rootTopicPrefix: %s", _rootTopicPrefix); 190 | sprintf(_willTopic, "%s/tele/LWT", _rootTopicPrefix); 191 | logd("_willTopic: %s", _willTopic); 192 | _mqttClient.setWill(_willTopic, 0, true, "Offline"); 193 | 194 | } 195 | } 196 | // generate unique id from mac address NIC segment 197 | uint8_t chipid[6]; 198 | esp_efuse_mac_get_default(chipid); 199 | _uniqueId = chipid[3] << 16; 200 | _uniqueId += chipid[4] << 8; 201 | _uniqueId += chipid[5]; 202 | // Set up required URL handlers on the web server. 203 | webServer.on("/settings", [this]() { 204 | if (_iotWebConf.handleCaptivePortal()) // -- Let IotWebConf test and handle captive portal requests. 205 | { 206 | logd("Captive portal"); // -- Captive portal request were already served. 207 | return; 208 | } 209 | logd("handleSettings"); 210 | std::stringstream ss; 211 | ss << ""; 212 | ss << _iotWebConf.getThingName(); 213 | ss << "

    "; 214 | ss << _iotWebConf.getThingName(); 215 | ss << " Settings

    "; 216 | ss << "
    Firmware config version "; 217 | ss << CONFIG_VERSION; 218 | ss << "

    "; 219 | ss << IOTCB()->getSettingsHTML().c_str(); 220 | ss << "

    MQTT:
      "; 221 | ss << htmlConfigEntry(mqttServerParam.label, mqttServerParam.value()).c_str(); 222 | ss << htmlConfigEntry(mqttPortParam.label, mqttPortParam.value()).c_str(); 223 | ss << htmlConfigEntry(mqttUserNameParam.label, mqttUserNameParam.value()).c_str(); 224 | ss << htmlConfigEntry(mqttUserPasswordParam.label, strlen(mqttUserPasswordParam.value()) > 0 ? "********" : "").c_str(); 225 | ss << htmlConfigEntry(mqttSubtopicParam.label, mqttSubtopicParam.value()).c_str(); 226 | ss << "

    Return to home page.

    "; 229 | ss << "

    Configuration

    *Log in with 'admin', AP password (default is 12345678)

    "; 230 | ss << "

    Web Log

    "; 233 | ss << "

    Firmware update

    "; 234 | ss << "

    Reboot ESP32

    "; 235 | ss << "
    \n"; 236 | std::string html = ss.str(); 237 | webServer.send(200, "text/html", html.c_str()); 238 | }); 239 | webServer.on("/config", []() { _iotWebConf.handleConfig(); }); 240 | webServer.on("/reboot", [this]() { 241 | logd("resetModule"); 242 | String page = reboot_html; 243 | webServer.send(200, "text/html", page.c_str()); 244 | delay(3000); 245 | esp_restart(); 246 | }); 247 | webServer.onNotFound([]() { _iotWebConf.handleNotFound(); }); 248 | webServer.on("/", []() { 249 | if (_iotWebConf.getState() == iotwebconf::NetworkState::NotConfigured) 250 | { 251 | _iotWebConf.handleConfig(); 252 | return; 253 | } 254 | String page = redirect_html; 255 | String url = "http://"; 256 | url += WiFi.localIP().toString(); 257 | url += ":"; 258 | url += ASYNC_WEBSERVER_PORT; 259 | page.replace("{h}", url.c_str()); 260 | page.replace("{hp}", String(WSOCKET_HOME_PORT)); 261 | webServer.send(200, "text/html", page.c_str()); 262 | }); 263 | if (_watchdogTimer == NULL) 264 | { 265 | _watchdogTimer = timerBegin(0, 80, true); // timer 0, div 80 266 | timerAttachInterrupt(_watchdogTimer, []() { esp_restart(); }, true); // attach callback 267 | timerAlarmWrite(_watchdogTimer, WATCHDOG_TIMER * 1000, false); // set time in us 268 | timerAlarmEnable(_watchdogTimer); // enable interrupt 269 | } 270 | } 271 | 272 | boolean IOT::Run() 273 | { 274 | bool rVal = false; 275 | if (_watchdogTimer != NULL) 276 | { 277 | timerWrite(_watchdogTimer, 0); // feed the watchdog 278 | } 279 | _iotWebConf.doLoop(); 280 | if (_clientsConfigured && WiFi.isConnected()) 281 | { 282 | rVal = _mqttClient.connected(); 283 | } 284 | else 285 | { 286 | if (Serial.peek() == '{') 287 | { 288 | String s = Serial.readStringUntil('}'); 289 | s += "}"; 290 | JsonDocument doc; 291 | DeserializationError err = deserializeJson(doc, s); 292 | if (err) 293 | { 294 | loge("deserializeJson() failed: %s", err.c_str()); 295 | } 296 | else 297 | { 298 | if (doc["ssid"].is() && doc["password"].is()) 299 | { 300 | iotwebconf::Parameter *p = _iotWebConf.getWifiSsidParameter(); 301 | strcpy(p->valueBuffer, doc["ssid"]); 302 | logd("Setting ssid: %s", p->valueBuffer); 303 | p = _iotWebConf.getWifiPasswordParameter(); 304 | strcpy(p->valueBuffer, doc["password"]); 305 | logd("Setting password: %s", p->valueBuffer); 306 | p = _iotWebConf.getApPasswordParameter(); 307 | strcpy(p->valueBuffer, DEFAULT_AP_PASSWORD); // reset to default AP password 308 | _iotWebConf.saveConfig(); 309 | esp_restart(); 310 | } 311 | else 312 | { 313 | logw("Received invalid json: %s", s.c_str()); 314 | } 315 | } 316 | } 317 | else 318 | { 319 | Serial.read(); // discard data 320 | } 321 | } 322 | return rVal; 323 | } 324 | 325 | unsigned long IOT::PublishRate() 326 | { 327 | return publishRateParam.value() * 1000; 328 | } 329 | 330 | boolean IOT::Publish(const char *subtopic, JsonDocument &payload, boolean retained) 331 | { 332 | String s; 333 | serializeJson(payload, s); 334 | return Publish(subtopic, s.c_str(), retained); 335 | } 336 | 337 | boolean IOT::Publish(const char *subtopic, const char *value, boolean retained) 338 | { 339 | boolean rVal = false; 340 | if (_mqttClient.connected()) 341 | { 342 | char buf[64]; 343 | sprintf(buf, "%s/stat/%s", _rootTopicPrefix, subtopic); 344 | rVal = _mqttClient.publish(buf, 0, retained, value) > 0; 345 | if (!rVal) 346 | { 347 | loge("**** Failed to publish MQTT message"); 348 | } 349 | } 350 | return rVal; 351 | } 352 | 353 | boolean IOT::Publish(const char *topic, float value, boolean retained) 354 | { 355 | char buf[256]; 356 | snprintf_P(buf, sizeof(buf), "%.1f", value); 357 | return Publish(topic, buf, retained); 358 | } 359 | 360 | boolean IOT::PublishMessage(const char *topic, JsonDocument &payload, boolean retained) 361 | { 362 | boolean rVal = false; 363 | if (_mqttClient.connected()) 364 | { 365 | String s; 366 | serializeJson(payload, s); 367 | rVal = _mqttClient.publish(topic, 0, retained, s.c_str(), s.length()) > 0; 368 | if (!rVal) 369 | { 370 | loge("**** Configuration payload exceeds MAX MQTT Packet Size, %d [%s] topic: %s", s.length(), s.c_str(), topic); 371 | } 372 | } 373 | return rVal; 374 | } 375 | 376 | boolean IOT::PublishHADiscovery(const char *bank, JsonDocument &payload) 377 | { 378 | boolean rVal = false; 379 | if (_mqttClient.connected()) 380 | { 381 | char topic[64]; 382 | sprintf(topic, "%s/device/%s/config", HOME_ASSISTANT_PREFIX, bank); 383 | rVal = PublishMessage(topic, payload, true); 384 | } 385 | return rVal; 386 | } 387 | 388 | std::string IOT::getRootTopicPrefix() 389 | { 390 | std::string s(_rootTopicPrefix); 391 | return s; 392 | }; 393 | 394 | std::string IOT::getSubtopicName() 395 | { 396 | std::string s(mqttSubtopicParam.value()); 397 | return s; 398 | }; 399 | 400 | std::string IOT::getThingName() 401 | { 402 | std::string s(_iotWebConf.getThingName()); 403 | return s; 404 | } 405 | 406 | void IOT::Online() 407 | { 408 | if (!_publishedOnline) 409 | { 410 | _publishedOnline = _mqttClient.publish(_willTopic, 0, true, "Online"); 411 | } 412 | } 413 | 414 | } // namespace PylonToMQTT -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/Pack.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include "Defines.h" 3 | #include "Pack.h" 4 | 5 | namespace PylonToMQTT 6 | { 7 | 8 | void Pack::PublishDiscovery() 9 | { 10 | if (ReadyToPublish()) 11 | { 12 | logd("Publishing discovery for %s", Name().c_str()); 13 | char buffer[STR_LEN]; 14 | char pack_id[STR_LEN]; 15 | sprintf(pack_id, "%s_%s", _psi->getSubtopicName().c_str(), _name.c_str()); 16 | JsonDocument doc; 17 | JsonObject device = doc["device"].to(); 18 | device["name"] = _name.c_str(); 19 | device["sw_version"] = CONFIG_VERSION; 20 | device["manufacturer"] = "ClassicDIY"; 21 | sprintf(buffer, "ESP32-Bit (%X)", _psi->getUniqueId()); 22 | device["model"] = buffer; 23 | 24 | JsonObject origin = doc["origin"].to(); 25 | origin["name"] = _psi->getSubtopicName().c_str(); 26 | 27 | JsonArray identifiers = device["identifiers"].to(); 28 | identifiers.add(pack_id); 29 | 30 | JsonObject components = doc["components"].to(); 31 | JsonObject PackVoltage = components["PackVoltage"].to(); 32 | PackVoltage["platform"] = "sensor"; 33 | PackVoltage["name"] = "PackVoltage"; 34 | PackVoltage["device_class"] = "voltage"; 35 | PackVoltage["unit_of_measurement"] = "V"; 36 | PackVoltage["value_template"] = "{{ value_json.PackVoltage.Reading }}"; 37 | sprintf(buffer, "%s_PackVoltage", pack_id); 38 | PackVoltage["unique_id"] = buffer; 39 | PackVoltage["icon"] = "mdi:lightning-bolt"; 40 | 41 | JsonObject PackCurrent = components["PackCurrent"].to(); 42 | PackCurrent["platform"] = "sensor"; 43 | PackCurrent["name"] = "PackCurrent"; 44 | PackCurrent["device_class"] = "current"; 45 | PackCurrent["unit_of_measurement"] = "A"; 46 | PackCurrent["value_template"] = "{{ value_json.PackCurrent.Reading }}"; 47 | sprintf(buffer, "%s_PackCurrent", pack_id); 48 | PackCurrent["unique_id"] = buffer; 49 | PackCurrent["icon"] = "mdi:current-dc"; 50 | 51 | JsonObject SOC = components["SOC"].to(); 52 | SOC["platform"] = "sensor"; 53 | SOC["name"] = "SOC"; 54 | SOC["device_class"] = "battery"; 55 | SOC["unit_of_measurement"] = "%"; 56 | SOC["value_template"] = "{{ value_json.SOC }}"; 57 | sprintf(buffer, "%s_SOC", pack_id); 58 | SOC["unique_id"] = buffer; 59 | 60 | JsonObject RemainingCapacity = components["RemainingCapacity"].to(); 61 | RemainingCapacity["platform"] = "sensor"; 62 | RemainingCapacity["name"] = "RemainingCapacity"; 63 | RemainingCapacity["unit_of_measurement"] = "Ah"; 64 | RemainingCapacity["value_template"] = "{{ value_json.RemainingCapacity }}"; 65 | sprintf(buffer, "%s_RemainingCapacity", pack_id); 66 | RemainingCapacity["unique_id"] = buffer; 67 | RemainingCapacity["icon"] = "mdi:ev-station"; 68 | 69 | char jsonElement[STR_LEN]; 70 | for (int i = 0; i < _numberOfTemps; i++) 71 | { 72 | if (i < _pTempKeys->size()) 73 | { 74 | sprintf(jsonElement, "{{ value_json.Temps.%s.Reading }}", _pTempKeys->at(i).c_str()); 75 | JsonObject TMP = components[_pTempKeys->at(i).c_str()].to(); 76 | TMP["platform"] = "sensor"; 77 | TMP["name"] = _pTempKeys->at(i).c_str(); 78 | TMP["device_class"] = "temperature"; 79 | TMP["unit_of_measurement"] = "°C"; 80 | TMP["value_template"] = jsonElement; 81 | sprintf(buffer, "%s_%s", pack_id, _pTempKeys->at(i).c_str()); 82 | TMP["unique_id"] = buffer; 83 | } 84 | } 85 | 86 | char entityName[STR_LEN]; 87 | for (int i = 0; i < _numberOfCells; i++) 88 | { 89 | sprintf(entityName, "Cell_%d", i + 1); 90 | sprintf(jsonElement, "{{ value_json.Cells.Cell_%d.Reading }}", i + 1); 91 | JsonObject CELL = components[entityName].to(); 92 | CELL["platform"] = "sensor"; 93 | CELL["name"] = entityName; 94 | CELL["device_class"] = "voltage"; 95 | CELL["unit_of_measurement"] = "V"; 96 | CELL["value_template"] = jsonElement; 97 | sprintf(buffer, "%s_%s", pack_id, entityName); 98 | CELL["unique_id"] = buffer; 99 | CELL["icon"] = "mdi:lightning-bolt"; 100 | } 101 | 102 | sprintf(buffer, "%s/stat/readings/%s", _psi->getRootTopicPrefix().c_str(), _name.c_str()); 103 | doc["state_topic"] = buffer; 104 | sprintf(buffer, "%s/tele/LWT", _psi->getRootTopicPrefix().c_str()); 105 | doc["availability_topic"] = buffer; 106 | doc["pl_avail"] = "Online"; 107 | doc["pl_not_avail"] = "Offline"; 108 | _psi->PublishHADiscovery(pack_id, doc); 109 | _discoveryPublished = true; 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/Pylon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "IotWebConfOptionalGroup.h" 4 | #include 5 | #include "Log.h" 6 | #include "WebLog.h" 7 | #include "HelperFunctions.h" 8 | #include "Defines.h" 9 | #include "Pylon.h" 10 | #include 11 | #include 12 | #include 13 | #include "html.h" 14 | 15 | namespace PylonToMQTT 16 | { 17 | WebLog _webLog = WebLog(); 18 | AsyncWebServer asyncServer(ASYNC_WEBSERVER_PORT); 19 | WebSocketsServer _webSocket = WebSocketsServer(WSOCKET_HOME_PORT); 20 | 21 | CommandInformation _infoCommands[] = {CommandInformation::GetVersionInfo, CommandInformation::GetBarCode, CommandInformation::None}; 22 | CommandInformation _readingsCommands[] = {CommandInformation::AnalogValueFixedPoint, CommandInformation::AlarmInfo, CommandInformation::None}; 23 | 24 | 25 | Pylon::Pylon() 26 | { 27 | _asyncSerial = new AsyncSerial(); 28 | _TempKeys = {"CellTemp1_4", "CellTemp5_8", "CellTemp9_12", "CellTemp13_16", "MOS_T", "ENV_T"}; 29 | } 30 | 31 | Pylon::~Pylon() 32 | { 33 | delete _asyncSerial; 34 | } 35 | 36 | String Pylon::getSettingsHTML() 37 | { 38 | String s; 39 | s += "ToDo"; 40 | return s; 41 | } 42 | 43 | iotwebconf::ParameterGroup *Pylon::parameterGroup() 44 | { 45 | return NULL; 46 | } 47 | 48 | bool Pylon::validate(iotwebconf::WebRequestWrapper *webRequestWrapper) 49 | { 50 | return true; 51 | } 52 | 53 | void Pylon::onMqttConnect(bool sessionPresent) 54 | { 55 | logd("onMqttConnect"); 56 | } 57 | void Pylon::onMqttMessage(char *topic, JsonDocument &doc) 58 | { 59 | logd("onMqttMessage %s", topic); 60 | } 61 | 62 | void Pylon::onWiFiConnect() 63 | { 64 | asyncServer.begin(); 65 | _webLog.begin(&asyncServer); 66 | _webSocket.begin(); 67 | _webSocket.onEvent([](uint8_t num, WStype_t type, uint8_t *payload, size_t length) 68 | { 69 | if (type == WStype_DISCONNECTED) 70 | { 71 | logi("[%u] Home Page Disconnected!\n", num); 72 | } 73 | else if (type == WStype_CONNECTED) 74 | { 75 | logi("[%u] Home Page Connected!\n", num); 76 | } }); 77 | 78 | asyncServer.on("/", HTTP_GET, [this](AsyncWebServerRequest *request) { 79 | String page = home_html; 80 | page.replace("{n}", _psi->getThingName().c_str()); 81 | page.replace("{v}", CONFIG_VERSION); 82 | page.replace("{cp}", String(IOTCONFIG_PORT)); 83 | 84 | request->send(200, "text/html", page); 85 | }); 86 | } 87 | 88 | void Pylon::Process() 89 | { 90 | _webLog.process(); 91 | _webSocket.loop(); 92 | return; 93 | } 94 | 95 | bool Pylon::Transmit() 96 | { 97 | bool sequenceComplete = false; 98 | if (_numberOfPacks == 0) 99 | { 100 | _root.clear(); 101 | send_cmd(0xFF, CommandInformation::GetPackCount); 102 | } 103 | else 104 | { 105 | _psi->Online(); // ensure online status is published now that we have a pack count 106 | if (_currentPack < _Packs.size()) 107 | { 108 | if (_Packs[_currentPack].InfoPublished() == false) 109 | { 110 | if (_infoCommands[_infoCommandIndex] != CommandInformation::None) 111 | { 112 | send_cmd(_currentPack + 1, _infoCommands[_infoCommandIndex]); 113 | } 114 | else 115 | { 116 | if (_root.size() > 0) 117 | { 118 | String s; 119 | serializeJson(_root, s); 120 | _root.clear(); 121 | char buf[64]; 122 | sprintf(buf, "info/Pack%d", _currentPack + 1); 123 | _psi->Publish(buf, s.c_str(), false); 124 | _Packs[_currentPack].SetInfoPublished(); 125 | } 126 | } 127 | _infoCommandIndex++; 128 | if (_infoCommandIndex == sizeof(_infoCommands)) 129 | { 130 | _infoCommandIndex = 0; 131 | _currentPack++; 132 | if (_currentPack == _numberOfPacks) 133 | { 134 | _currentPack = 0; 135 | sequenceComplete = true; 136 | } 137 | } 138 | } 139 | else 140 | { 141 | if (_readingsCommands[_readingsCommandIndex] != CommandInformation::None) 142 | { 143 | send_cmd(_currentPack + 1, _readingsCommands[_readingsCommandIndex]); 144 | } 145 | else 146 | { 147 | if (_root.size() > 0) 148 | { 149 | _Packs[_currentPack].PublishDiscovery(); // PublishDiscovery if ready and not already published 150 | String s; 151 | serializeJson(_root, s); 152 | _root.clear(); 153 | char buf[64]; 154 | sprintf(buf, "readings/Pack%d", _currentPack + 1); 155 | _psi->Publish(buf, s.c_str(), false); 156 | } 157 | } 158 | _readingsCommandIndex++; 159 | if (_readingsCommandIndex == sizeof(_readingsCommands)) 160 | { 161 | _readingsCommandIndex = 0; 162 | _currentPack++; 163 | if (_currentPack == _numberOfPacks) 164 | { 165 | _currentPack = 0; 166 | sequenceComplete = true; 167 | } 168 | } 169 | } 170 | } 171 | } 172 | return sequenceComplete; 173 | } 174 | 175 | uint16_t Pylon::get_frame_checksum(char *frame) 176 | { 177 | uint16_t sum = 0; 178 | uint16_t len = strlen(frame); 179 | for (int i = 0; i < len; i++) 180 | { 181 | sum += frame[i]; 182 | } 183 | sum = ~sum; 184 | sum %= 0x10000; 185 | sum += 1; 186 | return sum; 187 | } 188 | 189 | int Pylon::get_info_length(const char *info) 190 | { 191 | size_t lenid = strlen(info); 192 | if (lenid == 0) 193 | return 0; 194 | int lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf); 195 | int lenid_modulo = lenid_sum % 16; 196 | int lenid_invert_plus_one = 0b1111 - lenid_modulo + 1; 197 | return (lenid_invert_plus_one << 12) + lenid; 198 | } 199 | 200 | void Pylon::encode_cmd(char *frame, uint8_t address, uint8_t cid2, const char *info) 201 | { 202 | char sub_frame[64]; 203 | uint8_t cid1 = 0x46; 204 | sprintf(sub_frame, "%02X%02X%02X%02X%04X", 0x25, address, cid1, cid2, get_info_length(info)); 205 | strcat(sub_frame, info); 206 | sprintf(frame, "~%s%04X\r", sub_frame, get_frame_checksum(sub_frame)); 207 | return; 208 | } 209 | 210 | void Pylon::send_cmd(uint8_t address, CommandInformation cmd) 211 | { 212 | _currentCommand = cmd; 213 | char raw_frame[64]; 214 | memset(raw_frame, 0, 64); 215 | char bdevid[4]; 216 | sprintf(bdevid, "%02X", address); 217 | encode_cmd(raw_frame, address, cmd, bdevid); 218 | logd("send_cmd: %s", raw_frame); 219 | _asyncSerial->Send(cmd, (byte *)raw_frame, strlen(raw_frame)); 220 | } 221 | 222 | String Pylon::convert_ASCII(char *p) 223 | { 224 | String ascii = ""; 225 | String hex = p; 226 | for (size_t i = 0; i < strlen(p); i += 2) 227 | { 228 | String part = hex.substring(i, 2); 229 | ascii += strtol(part.c_str(), nullptr, 16); 230 | } 231 | return ascii; 232 | } 233 | 234 | unsigned char parse_hex(char c) 235 | { 236 | if ('0' <= c && c <= '9') 237 | return c - '0'; 238 | if ('A' <= c && c <= 'F') 239 | return c - 'A' + 10; 240 | if ('a' <= c && c <= 'f') 241 | return c - 'a' + 10; 242 | return 0; 243 | } 244 | 245 | std::vector parse_string(const std::string &s) 246 | { 247 | size_t size = s.size(); 248 | if (size % 2 != 0) 249 | size++; 250 | std::vector result(size / 2); 251 | for (std::size_t i = 0; i != size / 2; ++i) 252 | result[i] = 16 * parse_hex(s[2 * i]) + parse_hex(s[2 * i + 1]); 253 | return result; 254 | } 255 | 256 | int Pylon::ParseResponse(char *szResponse, size_t readNow, CommandInformation cmd) 257 | { 258 | if (readNow > 0 && szResponse[0] != '\0') 259 | { 260 | logd("received: %d", readNow); 261 | logd("data: %s", szResponse); 262 | std::string chksum; 263 | chksum.assign(&szResponse[readNow - 4]); 264 | std::vector cs = parse_string(chksum); 265 | int i = 0; 266 | uint16_t CHKSUM = toShort(i, cs); 267 | uint16_t sum = 0; 268 | for (int i = 1; i < readNow - 4; i++) 269 | { 270 | sum += szResponse[i]; 271 | } 272 | if (((CHKSUM + sum) & 0xFFFF) != 0) 273 | { 274 | uint16_t c = ~sum + 1; 275 | loge("Checksum failed: %04x, should be: %04X", sum, c); 276 | return -1; 277 | } 278 | 279 | std::string frame; 280 | frame.assign(&szResponse[1]); // skip SOI (~) 281 | std::vector v = parse_string(frame); 282 | int index = 0; 283 | uint16_t VER = v[index++]; 284 | uint16_t ADR = v[index++]; 285 | uint16_t CID1 = v[index++]; 286 | uint16_t CID2 = v[index++]; 287 | uint16_t LENGTH = toShort(index, v); 288 | uint16_t LCHKSUM = LENGTH & 0xF000; 289 | uint16_t LENID = LENGTH & 0x0FFF; 290 | if (readNow < (LENID + 17)) 291 | { 292 | loge("Data length error LENGTH: %04X LENID: %04X, Received: %d", LENGTH, LENID, (readNow - 17)); 293 | return -1; 294 | } 295 | logd("VER: %02X, ADR: %02X, CID1: %02X, CID2: %02X, LENID: %02X (%d), CHKSUM: %02X", VER, ADR, CID1, CID2, LENID, LENID, CHKSUM); 296 | if (CID2 != ResponseCode::Normal) 297 | { 298 | loge("CID2 error code: %02X", CID2); 299 | return -1; 300 | } 301 | switch (cmd) 302 | { 303 | case CommandInformation::AnalogValueFixedPoint: 304 | { 305 | uint16_t INFO = toShort(index, v); 306 | uint16_t packNumber = INFO & 0x00FF; 307 | logi("AnalogValueFixedPoint: INFO: %04X, Pack: %d", INFO, packNumber); 308 | JsonObject cells = _root["Cells"].to(); 309 | char key[16]; 310 | uint16_t numberOfCells = v[index++]; 311 | for (int i = 0; i < numberOfCells; i++) 312 | { 313 | sprintf(key, "Cell_%d", i + 1); 314 | JsonObject cell = cells[key].to(); 315 | cell["Reading"] = (toShort(index, v)) / 1000.0; 316 | cell["State"] = 0xF0; 317 | } 318 | JsonObject temps = _root["Temps"].to(); 319 | uint16_t numberOfTemps = v[index++]; 320 | for (int i = 0; i < numberOfTemps; i++) 321 | { 322 | if (i < _TempKeys.size()) 323 | { 324 | JsonObject temp = temps[_TempKeys[i]].to(); 325 | float kelvin = (toShort(index, v))-2730.0; // use 273.0 instead of 273.15 to match jakiper app 326 | temp["Reading"] = round(kelvin) / 10.0; // limit to one decimal place 327 | temp["State"] = 0; // default to ok 328 | } 329 | } 330 | int packIndex = packNumber - 1; 331 | logd("AnalogValueFixedPoint: packIndex: %d, Pack size: %d", packIndex, _Packs.size()); 332 | if (packIndex < _Packs.size()) 333 | { 334 | _Packs[packIndex].setNumberOfCells(numberOfCells); 335 | _Packs[packIndex].setNumberOfTemps(numberOfTemps); 336 | } 337 | JsonObject PackCurrent = _root["PackCurrent"].to(); 338 | float current = ((int16_t)toShort(index, v)) / 100.0; 339 | PackCurrent["Reading"] = current; 340 | PackCurrent["State"] = 0; // default to ok 341 | JsonObject PackVoltage = _root["PackVoltage"].to(); 342 | float voltage = (toShort(index, v)) / 1000.0; 343 | PackVoltage["Reading"] = voltage; 344 | PackVoltage["State"] = 0; // default to ok 345 | int remain = toShort(index, v); 346 | _root["RemainingCapacity"] = (remain / 100.0); 347 | index++; // skip user def code 348 | int total = toShort(index, v); 349 | _root["FullCapacity"] = (total / 100.0); 350 | _root["CycleCount"] = ((v[index++] << 8) | v[index++]); 351 | _root["SOC"] = (remain * 100) / total; 352 | _root["Power"] = round(voltage * current); 353 | // module["LAST"] = ((v[index++]<<8) | (v[index++]<<8) | v[index++]); 354 | } 355 | break; 356 | case CommandInformation::GetVersionInfo: 357 | { 358 | std::string ver; 359 | std::string s(v.begin(), v.end()); 360 | ver = s.substr(index); 361 | _root["Version"] = ver.substr(0, 19); 362 | int packIndex = ADR - 1; 363 | if (packIndex < _Packs.size()) 364 | { 365 | _Packs[packIndex].setVersionInfo(ver); 366 | } 367 | } 368 | break; 369 | case CommandInformation::AlarmInfo: 370 | { 371 | uint16_t INFO = toShort(index, v); 372 | uint16_t packNumber = INFO & 0x00FF; 373 | JsonObject cells = _root["Cells"].as(); 374 | logi("GetAlarm: Pack: %d", packNumber); 375 | char key[16]; 376 | uint16_t numberOfCells = v[index++]; 377 | for (int i = 0; i < numberOfCells; i++) 378 | { 379 | sprintf(key, "Cell_%d", i + 1); 380 | JsonObject cell = cells[key].as(); 381 | cell["State"] = v[index++]; 382 | } 383 | JsonObject temps = _root["Temps"].as(); 384 | uint16_t numberOfTemps = v[index++]; 385 | for (int i = 0; i < numberOfTemps; i++) 386 | { 387 | if (i < _TempKeys.size()) 388 | { 389 | JsonObject entry = temps[_TempKeys[i]].as(); 390 | entry["State"] = v[index++]; 391 | } 392 | } 393 | index++; // skip 65 394 | JsonObject entry = _root["PackCurrent"].as(); 395 | entry["State"] = v[index++]; 396 | entry = _root["PackVoltage"].as(); 397 | entry["State"] = v[index++]; 398 | uint8_t ProtectSts1 = v[index++]; 399 | uint8_t ProtectSts2 = v[index++]; 400 | uint8_t SystemSts = v[index++]; 401 | uint8_t FaultSts = v[index++]; 402 | index++; // skip 81 403 | index++; // skip 83 404 | uint8_t AlarmSts1 = v[index++]; 405 | uint8_t AlarmSts2 = v[index++]; 406 | 407 | JsonObject pso = _root["Protect_Status"].to(); 408 | pso["Charger_OVP"] = CheckBit(ProtectSts1, 7); 409 | pso["SCP"] = CheckBit(ProtectSts1, 6); 410 | pso["DSG_OCP"] = CheckBit(ProtectSts1, 5); 411 | pso["CHG_OCP"] = CheckBit(ProtectSts1, 4); 412 | pso["Pack_UVP"] = CheckBit(ProtectSts1, 3); 413 | pso["Pack_OVP"] = CheckBit(ProtectSts1, 2); 414 | pso["Cell_UVP"] = CheckBit(ProtectSts1, 1); 415 | pso["Cell_OVP"] = CheckBit(ProtectSts1, 0); 416 | pso["ENV_UTP"] = CheckBit(ProtectSts2, 6); 417 | pso["ENV_OTP"] = CheckBit(ProtectSts2, 5); 418 | pso["MOS_OTP"] = CheckBit(ProtectSts2, 4); 419 | pso["DSG_UTP"] = CheckBit(ProtectSts2, 3); 420 | pso["CHG_UTP"] = CheckBit(ProtectSts2, 2); 421 | pso["DSG_OTP"] = CheckBit(ProtectSts2, 1); 422 | pso["CHG_OTP"] = CheckBit(ProtectSts2, 0); 423 | 424 | JsonObject sso = _root["System_Status"].to(); 425 | sso["Fully_Charged"] = CheckBit(ProtectSts2, 7); 426 | sso["Heater"] = CheckBit(SystemSts, 7); 427 | sso["AC_in"] = CheckBit(SystemSts, 5); 428 | sso["Discharge_MOS"] = CheckBit(SystemSts, 2); 429 | sso["Charge_MOS"] = CheckBit(SystemSts, 1); 430 | sso["Charge_Limit"] = CheckBit(SystemSts, 0); 431 | 432 | JsonObject fso = _root["Fault_Status"].to(); 433 | fso["Heater_Fault"] = CheckBit(FaultSts, 7); 434 | fso["CCB_Fault"] = CheckBit(FaultSts, 6); 435 | fso["Sampling_Fault"] = CheckBit(FaultSts, 5); 436 | fso["Cell_Fault"] = CheckBit(FaultSts, 4); 437 | fso["NTC_Fault"] = CheckBit(FaultSts, 2); 438 | fso["DSG_MOS_Fault"] = CheckBit(FaultSts, 1); 439 | fso["CHG_MOS_Fault"] = CheckBit(FaultSts, 0); 440 | 441 | JsonObject aso = _root["Alarm_Status"].to(); 442 | aso["DSG_OC"] = CheckBit(AlarmSts1, 5); 443 | aso["CHG_OC"] = CheckBit(AlarmSts1, 4); 444 | aso["Pack_UV"] = CheckBit(AlarmSts1, 3); 445 | aso["Pack_OV"] = CheckBit(AlarmSts1, 2); 446 | aso["Cell_UV"] = CheckBit(AlarmSts1, 1); 447 | aso["Cell_OV"] = CheckBit(AlarmSts1, 0); 448 | 449 | aso["SOC_Low"] = CheckBit(AlarmSts2, 7); 450 | aso["MOS_OT"] = CheckBit(AlarmSts2, 6); 451 | aso["ENV_UT"] = CheckBit(AlarmSts2, 5); 452 | aso["ENV_OT"] = CheckBit(AlarmSts2, 4); 453 | aso["DSG_UT"] = CheckBit(AlarmSts2, 3); 454 | aso["CHG_UT"] = CheckBit(AlarmSts2, 2); 455 | aso["DSG_OT"] = CheckBit(AlarmSts2, 1); 456 | aso["CHG_OT"] = CheckBit(AlarmSts2, 0); 457 | } 458 | break; 459 | case CommandInformation::GetBarCode: 460 | { 461 | std::string bc; 462 | std::string s(v.begin(), v.end() - 2); 463 | bc = s.substr(index); 464 | logi("GetBarCode for %d bc: %s", ADR, bc.c_str()); 465 | _root["BarCode"] = bc.substr(0, 15); 466 | int packIndex = ADR - 1; 467 | if (packIndex < _Packs.size()) 468 | { 469 | _Packs[packIndex].setBarcode(bc.substr(0, 15)); 470 | } 471 | } 472 | break; 473 | case CommandInformation::GetPackCount: 474 | { 475 | _numberOfPacks = v[index]; 476 | _numberOfPacks > 8 ? 1 : _numberOfPacks; // max 8, default to 1 477 | _root.clear(); 478 | logi("GetPackCount: %d", _numberOfPacks); 479 | for (int i = 0; i < _numberOfPacks; i++) 480 | { 481 | char packName[STR_LEN]; 482 | sprintf(packName, "Pack%d", i + 1); 483 | _Packs.push_back(Pack(packName, &_TempKeys, _psi)); 484 | } 485 | } 486 | break; 487 | } 488 | } 489 | return 0; 490 | } 491 | 492 | } // namespace PylonToMQTT -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/WebLog.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include "WebLog.h" 3 | 4 | WebSocketsServer _webSocket = WebSocketsServer(WSOCKET_LOG_PORT); 5 | 6 | int weblog_log_printfv(const char *format, va_list arg) 7 | { 8 | static char loc_buf[256]; 9 | char * temp = loc_buf; 10 | uint32_t len; 11 | va_list copy; 12 | va_copy(copy, arg); 13 | len = vsnprintf(NULL, 0, format, copy); 14 | va_end(copy); 15 | if(len >= sizeof(loc_buf)){ 16 | temp = (char*)malloc(len+1); 17 | if(temp == NULL) { 18 | return 0; 19 | } 20 | } 21 | vsnprintf(temp, len+1, format, arg); 22 | _webSocket.broadcastTXT(temp); 23 | if(len >= sizeof(loc_buf)){ 24 | free(temp); 25 | } 26 | int rVal = ets_printf("%s", temp); 27 | return rVal; 28 | } 29 | 30 | int weblog(const char *format, ...) 31 | { 32 | int len; 33 | va_list arg; 34 | va_start(arg, format); 35 | len = weblog_log_printfv(format, arg); 36 | va_end(arg); 37 | return len; 38 | } 39 | 40 | void WebLog::begin(AsyncWebServer *pwebServer) 41 | { 42 | _webSocket.begin(); 43 | _webSocket.onEvent([](uint8_t num, WStype_t type, uint8_t *payload, size_t length) 44 | { 45 | if (type == WStype_DISCONNECTED) 46 | { 47 | logi("[%u] Web Log Disconnected!\n", num); 48 | } 49 | else if (type == WStype_CONNECTED) 50 | { 51 | logi("[%u] Web Log Connected!\n", num); 52 | } }); 53 | pwebServer->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) 54 | { request->send(200, "text/html", web_serial_html); }); 55 | } 56 | 57 | void WebLog::process() 58 | { 59 | _webSocket.loop(); 60 | } 61 | -------------------------------------------------------------------------------- /Code/ESP32/PylonToMQTT/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "Enumerations.h" 5 | #include "Log.h" 6 | #include "IOT.h" 7 | #include "Pylon.h" 8 | 9 | using namespace PylonToMQTT; 10 | 11 | IOT _iot = IOT(); 12 | unsigned long _lastPublishTimeStamp = 0; 13 | Pylon _Pylon = Pylon(); 14 | 15 | void setup() 16 | { 17 | Serial.begin(115200); 18 | while (!Serial) {} 19 | _lastPublishTimeStamp = millis() + COMMAND_PUBLISH_RATE; 20 | // Set up object used to communicate with battery, provide callback to MQTT publish 21 | _Pylon.begin(&_iot); 22 | _iot.Init(&_Pylon); 23 | logd("Setup Done"); 24 | } 25 | 26 | void loop() 27 | { 28 | _Pylon.Process(); 29 | if (_iot.Run()) { 30 | _Pylon.Receive(SERIAL_RECEIVE_TIMEOUT); 31 | if (_lastPublishTimeStamp < millis()) 32 | { 33 | unsigned long currentPublishRate = _Pylon.Transmit() == true ? _iot.PublishRate() : COMMAND_PUBLISH_RATE; 34 | _lastPublishTimeStamp = millis() + currentPublishRate; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Code/ESP32/README.md: -------------------------------------------------------------------------------- 1 | # ESP32 PylonTech MQTT Publisher 2 | 3 |

    4 | 5 |

    6 | 7 |

    8 | Please refer to the ClassicMQTT wiki for more information. 9 |

    10 | 11 | 12 | Release notes for the ESP32 implementation: 13 | 14 | ----------------- 15 | version 1.1 16 | 17 |
      18 |
    • Initial Release
    • 19 |
    20 | 21 | ----------------- 22 | 23 | Release notes for the Raspberry Pi implementation: 24 | 25 | ----------------- 26 | 27 | -------------------------------------------------------------------------------- /Code/Python/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Files 10 | simple_run_classic.sh 11 | mosquitto.db 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | .vscode 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ -------------------------------------------------------------------------------- /Code/Python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.6-slim-stretch 2 | 3 | RUN pip install --no-cache-dir pyserial paho-mqtt construct 4 | 5 | ADD pylon_to_mqtt.py / 6 | ADD support/*.py support/ 7 | 8 | ENTRYPOINT ["python3", "pylon_to_mqtt.py"] -------------------------------------------------------------------------------- /Code/Python/IOTStack_docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | telegraf: 4 | container_name: telegraf 5 | build: ./.templates/telegraf/. 6 | restart: unless-stopped 7 | environment: 8 | - TZ=Etc/UTC 9 | volumes: 10 | - ./volumes/telegraf:/etc/telegraf 11 | # - /var/run/docker.sock:/var/run/docker.sock:ro 12 | depends_on: 13 | - influxdb 14 | - mosquitto 15 | networks: 16 | - iotstack_nw 17 | ports: 18 | - "8092:8092/udp" 19 | - "8094:8094/tcp" 20 | - "8125:8125/udp" 21 | 22 | grafana: 23 | container_name: grafana 24 | image: grafana/grafana 25 | restart: unless-stopped 26 | user: "0" 27 | ports: 28 | - "3000:3000" 29 | environment: 30 | - GF_PATHS_DATA=/var/lib/grafana 31 | - GF_PATHS_LOGS=/var/log/grafana 32 | volumes: 33 | - ./volumes/grafana/data:/var/lib/grafana 34 | - ./volumes/grafana/log:/var/log/grafana 35 | networks: 36 | - iotstack_nw 37 | 38 | heimdall: 39 | image: ghcr.io/linuxserver/heimdall 40 | container_name: heimdall 41 | environment: 42 | - PUID=1000 43 | - PGID=1000 44 | - TZ=USA/Boston 45 | volumes: 46 | - ./volumes/heimdall/config:/config 47 | ports: 48 | - 8880:80 49 | - 8883:443 50 | restart: unless-stopped 51 | 52 | influxdb: 53 | container_name: influxdb 54 | image: "influxdb:1.8" 55 | restart: unless-stopped 56 | ports: 57 | - "8086:8086" 58 | environment: 59 | - INFLUXDB_HTTP_FLUX_ENABLED=false 60 | - INFLUXDB_REPORTING_DISABLED=false 61 | - INFLUXDB_HTTP_AUTH_ENABLED=false 62 | - INFLUX_USERNAME=dba 63 | - INFLUX_PASSWORD=supremo 64 | - INFLUXDB_UDP_ENABLED=false 65 | - INFLUXDB_UDP_BIND_ADDRESS=0.0.0.0:8086 66 | - INFLUXDB_UDP_DATABASE=udp 67 | volumes: 68 | - ./volumes/influxdb/data:/var/lib/influxdb 69 | - ./backups/influxdb/db:/var/lib/influxdb/backup 70 | networks: 71 | - iotstack_nw 72 | 73 | mosquitto: 74 | container_name: mosquitto 75 | build: ./.templates/mosquitto/. 76 | restart: unless-stopped 77 | environment: 78 | - TZ=Etc/UTC 79 | ports: 80 | - "1883:1883" 81 | volumes: 82 | - ./volumes/mosquitto/config:/mosquitto/config 83 | - ./volumes/mosquitto/data:/mosquitto/data 84 | - ./volumes/mosquitto/log:/mosquitto/log 85 | - ./volumes/mosquitto/pwfile:/mosquitto/pwfile 86 | networks: 87 | - iotstack_nw 88 | 89 | portainer-ce: 90 | container_name: portainer-ce 91 | image: portainer/portainer-ce:latest 92 | restart: unless-stopped 93 | ports: 94 | - "8000:8000" 95 | - "9000:9000" 96 | volumes: 97 | - /var/run/docker.sock:/var/run/docker.sock 98 | - ./volumes/portainer-ce/data:/data 99 | home_assistant: 100 | container_name: home_assistant 101 | image: ghcr.io/home-assistant/home-assistant:stable 102 | #image: ghcr.io/home-assistant/raspberrypi3-homeassistant:stable 103 | #image: ghcr.io/home-assistant/raspberrypi4-homeassistant:stable 104 | restart: unless-stopped 105 | network_mode: host 106 | volumes: 107 | - /etc/localtime:/etc/localtime:ro 108 | - ./volumes/home_assistant:/config 109 | 110 | networks: 111 | iotstack_nw: # Exposed by your host. 112 | # external: true 113 | name: IOTstack_Net 114 | driver: bridge 115 | ipam: 116 | driver: default 117 | config: 118 | - subnet: 10.77.60.0/24 119 | # - gateway: 10.77.60.1 120 | 121 | iotstack_nw_internal: # For interservice communication. No access to outside 122 | name: IOTstack_Net_Internal 123 | driver: bridge 124 | internal: true 125 | ipam: 126 | driver: default 127 | config: 128 | - subnet: 10.77.76.0/24 129 | # - gateway: 10.77.76.1 130 | vpn_nw: # Network specifically for VPN 131 | name: IOTstack_VPN 132 | driver: bridge 133 | ipam: 134 | driver: default 135 | config: 136 | - subnet: 10.77.88.0/24 137 | # - gateway: 192.18.200.1 138 | 139 | nextcloud_internal: # Network for NextCloud service 140 | name: IOTstack_NextCloud 141 | driver: bridge 142 | internal: true 143 | -------------------------------------------------------------------------------- /Code/Python/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Graham Ross 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Code/Python/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Pylontech MQTT Publisher Python Implementation 3 | 4 | The code in this repository will read data from your Jakiper Battery console interface and publish it to an MQTT broker. It is a read-only program with respect to the battery BMS, it does not write any data to BMS. It is intended to be used with InfluxDb and Grafana. 5 | 6 | The software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied. 7 | 8 |

    9 | Please refer to the PylonToMQTT Python Implementation wiki for more information. 10 |

    11 | 12 | -------------------------------------------------------------------------------- /Code/Python/compose-override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pylon_to_mqtt: 3 | container_name: pylon_to_mqtt 4 | image: classicdiy/pylon_to_mqtt:latest 5 | restart: unless-stopped 6 | devices: 7 | - /dev/ttyUSB0:/dev/ttyUSB0 8 | depends_on: 9 | - mosquitto 10 | environment: 11 | - LOGLEVEL=INFO 12 | - PYLON_PORT=/dev/ttyUSB0 13 | - RACK_NAME=Main 14 | - MQTT_HOST=mosquitto 15 | - MQTT_PORT=1883 16 | - MQTT_ROOT=PylonToMQTT 17 | # - MQTT_USER=ClassicPublisher 18 | # - MQTT_PASS=ClassicPub123 19 | # - SOK=true 20 | 21 | 22 | -------------------------------------------------------------------------------- /Code/Python/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mosquitto: 4 | image: eclipse-mosquitto 5 | hostname: mosquitto 6 | container_name: mosquitto 7 | restart: unless-stopped 8 | expose: 9 | - "1883" 10 | - "9001" 11 | ports: 12 | - "1883:1883" 13 | - "9001:9001" 14 | volumes: 15 | - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro 16 | - ./mosquitto/mosquitto.passwd:/mosquitto/config/mosquitto.passwd 17 | networks: 18 | - localnet 19 | 20 | 21 | pylon_to_mqtt: 22 | container_name: pylon_to_mqtt 23 | image: pylon_to_mqtt 24 | restart: unless-stopped 25 | build: . 26 | environment: 27 | - LOGLEVEL=DEBUG 28 | - PYLON_PORT=/dev/ttyUSB0 29 | - RACK_NAME=Main 30 | - MQTT_HOST=mosquitto 31 | - MQTT_PORT=1883 32 | - MQTT_ROOT=PylonToMQTT 33 | - MQTT_USER=ClassicPublisher 34 | - MQTT_PASS=ClassicPub123 35 | - PUBLISH_RATE=5 36 | 37 | depends_on: 38 | - mosquitto 39 | networks: 40 | - localnet 41 | 42 | # Note you no longer need to create your own .env file. 43 | # Simply replace your particular values in the "environment" section above. 44 | # Note, in the mosquitto.passwd file in the repo there are 2 users: 45 | # ClassicPublisher password = ClassicPub123 46 | # ClassicClient password= ClassicClient123 47 | # 48 | # After you have set the values in the environment section above, use the 49 | # following command to build and run it: 50 | # docker-compose -f classic_mqtt_compose.yml up 51 | # 52 | # If you change the values in this file after you use the above 53 | # docker-compose command, use this command to re-build it. 54 | # docker-compose -f classic_mqtt_compose.yml build 55 | # 56 | 57 | 58 | networks: 59 | localnet: -------------------------------------------------------------------------------- /Code/Python/pylon_to_mqtt.py: -------------------------------------------------------------------------------- 1 | from paho.mqtt import client as mqttclient 2 | from collections import OrderedDict 3 | import json 4 | import time 5 | import threading 6 | import logging 7 | import os 8 | import sys 9 | from random import randint, seed 10 | from enum import Enum 11 | 12 | from support.pylon_jsonencoder import encodePylon_readings, encodePylon_info 13 | from support.pylon_validate import handleArgs 14 | from support.pylontech import Pylontech 15 | from support.pylontech import PylonTechSOK 16 | from time import time_ns 17 | 18 | # --------------------------------------------------------------------------- # 19 | # GLOBALS 20 | # --------------------------------------------------------------------------- # 21 | MAX_PUBLISH_RATE = 15 #in seconds 22 | MIN_PUBLISH_RATE = 3 #in seconds 23 | DEFAULT_WAKE_RATE = 5 #in seconds 24 | MQTT_MAX_ERROR_COUNT = 300 #Number of errors on the MQTT before the tool exits 25 | MAIN_LOOP_SLEEP_SECS = 5 #Seconds to sleep in the main loop 26 | CONFIG_VERSION = "V1.2.1" # major.minor.build (major or minor will invalidate the configuration) 27 | HOME_ASSISTANT_PREFIX = "homeassistant" # MQTT prefix used in autodiscovery 28 | 29 | tempKeys = ["CellTemp1_4", "CellTemp5_8", "CellTemp9_12", "CellTemp13_16", "MOS_T", "ENV_T"] 30 | 31 | # --------------------------------------------------------------------------- # 32 | # Default startup values. Can be over-ridden by command line options. 33 | # --------------------------------------------------------------------------- # 34 | argumentValues = { \ 35 | 'pylonPort':os.getenv('PYLON_PORT', "/dev/ttyUSB0"), \ 36 | 'baud_rate':os.getenv('PYLON_BAUD_RATE', "9600"), \ 37 | 'rackName':os.getenv('RACK_NAME', "Main"), \ 38 | 'mqttHost':os.getenv('MQTT_HOST', "mosquitto"), \ 39 | 'mqttPort':os.getenv('MQTT_PORT', "1883"), \ 40 | 'mqttRoot':os.getenv('MQTT_ROOT', "PylonToMQTT"), \ 41 | 'mqttUser':os.getenv('MQTT_USER', ""), \ 42 | 'mqttPassword':os.getenv('MQTT_PASS', ""), \ 43 | 'publishRate':int(os.getenv('PUBLISH_RATE', str(DEFAULT_WAKE_RATE))), \ 44 | 'sok':bool(os.getenv("SOK", "")) # default is false (Jakiper battery) 45 | } 46 | 47 | # --------------------------------------------------------------------------- # 48 | # Counters and status variables 49 | # --------------------------------------------------------------------------- # 50 | infoPublished = False 51 | mqttConnected = False 52 | doStop = False 53 | mqttErrorCount = 0 54 | currentPollRate = DEFAULT_WAKE_RATE 55 | mqttClient = None 56 | number_of_packs = 0 57 | current_pack_index = 0 58 | info_published = None 59 | discovery_published = None 60 | pack_versions = None 61 | pack_barcodes = None 62 | pylontech = None 63 | 64 | # --------------------------------------------------------------------------- # 65 | # configure the logging 66 | # --------------------------------------------------------------------------- # 67 | log = logging.getLogger("PylonToMQTT") 68 | if not log.handlers: 69 | handler = logging.StreamHandler(sys.stdout) 70 | formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s') 71 | handler.setFormatter(formatter) 72 | log.addHandler(handler) 73 | log.setLevel(os.environ.get("LOGLEVEL", "INFO")) 74 | 75 | # --------------------------------------------------------------------------- # 76 | # MQTT On Connect function 77 | # --------------------------------------------------------------------------- # 78 | def on_connect(client, userdata, flags, rc): 79 | global mqttConnected, mqttErrorCount, mqttClient 80 | if rc==0: 81 | log.info("MQTT connected OK Returned code={}".format(rc)) 82 | #subscribe to the commands 83 | try: 84 | topic = "{}{}/cmnd/#".format(argumentValues['mqttRoot'], argumentValues['rackName']) 85 | client.subscribe(topic) 86 | log.info("Subscribed to {}".format(topic)) 87 | 88 | #publish that we are Online 89 | will_topic = "{}{}/tele/LWT".format(argumentValues['mqttRoot'], argumentValues['rackName']) 90 | mqttClient.publish(will_topic, "Online", qos=0, retain=False) 91 | except Exception as e: 92 | log.error("MQTT Subscribe failed") 93 | log.exception(e, exc_info=True) 94 | 95 | mqttConnected = True 96 | mqttErrorCount = 0 97 | else: 98 | mqttConnected = False 99 | log.error("MQTT Bad connection Returned code={}".format(rc)) 100 | 101 | # --------------------------------------------------------------------------- # 102 | # MQTT On Disconnect 103 | # --------------------------------------------------------------------------- # 104 | def on_disconnect(client, userdata, rc): 105 | global mqttConnected, mqttClient 106 | mqttConnected = False 107 | #if disconnetion was unexpectred (not a result of a disconnect request) then log it. 108 | if rc!=mqttclient.MQTT_ERR_SUCCESS: 109 | log.info("on_disconnect: Disconnected. ReasonCode={}".format(mqttclient.error_string(rc))) 110 | 111 | # --------------------------------------------------------------------------- # 112 | # MQTT On Message 113 | # --------------------------------------------------------------------------- # 114 | def on_message(client, userdata, message): 115 | #print("Received message '" + str(message.payload) + "' on topic '" 116 | #+ message.topic + "' with QoS " + str(message.qos)) 117 | 118 | global currentPollRate, infoPublished, doStop, mqttConnected, mqttErrorCount, argumentValues 119 | 120 | mqttConnected = True #got a message so we must be up again... 121 | mqttErrorCount = 0 122 | 123 | msg = message.payload.decode(encoding='UTF-8').upper() 124 | log.info("Received MQTT message {}".format(msg)) 125 | 126 | if msg == "{\"STOP\"}": 127 | doStop = True 128 | else: #JSON messages 129 | theMessage = json.loads(message.payload.decode(encoding='UTF-8')) 130 | log.debug(theMessage) 131 | if "publishRate" in theMessage: 132 | newRate_msecs = theMessage['publishRate'] 133 | newRate = round(newRate_msecs/1000) 134 | if newRate < MIN_PUBLISH_RATE: 135 | log.error("Received publishRate of {} which is below minimum of {}".format(newRate,MIN_PUBLISH_RATE)) 136 | elif newRate > MAX_PUBLISH_RATE: 137 | log.error("Received publishRate of {} which is above maximum of {}".format(newRate,MAX_PUBLISH_RATE)) 138 | else: 139 | argumentValues['publishRate'] = newRate 140 | currentPollRate = newRate 141 | log.info("publishRate message received, setting rate to {}".format(newRate)) 142 | else: 143 | log.error("on_message: Received something else") 144 | 145 | # --------------------------------------------------------------------------- # 146 | # MQTT Publish the data 147 | # --------------------------------------------------------------------------- # 148 | def mqttPublish(data, subtopic, retain): 149 | global mqttConnected, mqttClient, mqttErrorCount 150 | 151 | topic = "{}{}/stat/{}".format(argumentValues['mqttRoot'], argumentValues['rackName'], subtopic) 152 | log.info("Publishing: {}".format(topic)) 153 | 154 | try: 155 | mqttClient.publish(topic, data, qos=0, retain=retain) 156 | return True 157 | except Exception as e: 158 | log.error("MQTT Publish Error Topic:{}".format(topic)) 159 | log.exception(e, exc_info=True) 160 | mqttConnected = False 161 | mqttErrorCount += 1 162 | return False 163 | 164 | def PublishDiscoverySub(component, entity, jsonElement, device_class, unit_of_meas, icon=0): 165 | global current_pack_index, pack_barcodes, pack_versions 166 | 167 | current_pack_number = current_pack_index + 1 # pack number is origin 1 168 | doc = {} 169 | doc["device_class"] = device_class 170 | doc["unit_of_measurement"] = unit_of_meas 171 | doc["state_class"] = "measurement" 172 | doc["name"] = entity 173 | if (icon): 174 | doc["icon"] = icon 175 | doc["state_topic"] = "{}{}/stat/readings/Pack{}".format(argumentValues['mqttRoot'], argumentValues['rackName'], current_pack_number) 176 | object_id = "Rpi_Pack{}_{}".format(current_pack_number, entity) 177 | doc["unique_id"] = object_id 178 | doc["value_template"] = "{{{{ value_json.{} }}}}".format(jsonElement) 179 | doc["availability_topic"] = "{}{}/tele/LWT".format(argumentValues['mqttRoot'], argumentValues['rackName']) 180 | doc["pl_avail"] = "Online" 181 | doc["pl_not_avail"] = "Offline" 182 | device = {} 183 | device["name"] = "Pack{}".format(current_pack_number) 184 | device["via_device"] = argumentValues['mqttRoot'][:-1] 185 | device["hw_version"] = pack_barcodes[current_pack_index] 186 | device["sw_version"] = CONFIG_VERSION 187 | device["manufacturer"] = "ClassicDIY" 188 | device["model"] = pack_versions[current_pack_index] 189 | device["identifiers"] = "Pack{}_{}".format(current_pack_number, pack_barcodes[current_pack_index]) 190 | doc["device"] = device 191 | mqttClient.publish("{}/{}/{}/config".format(HOME_ASSISTANT_PREFIX, component, object_id),json.dumps(doc, sort_keys=False, separators=(',', ':')), qos=0, retain=False) 192 | 193 | def PublishTempsDiscovery(numberOfTemps): 194 | for x in range(numberOfTemps): 195 | tempKey = "Temp{}".format(x) 196 | if (x < len(tempKeys)): 197 | tempKey = tempKeys[x] 198 | PublishDiscoverySub("sensor", tempKey, "Temps.{}.Reading".format(tempKey), "temperature", "°C") 199 | 200 | def PublishCellsDiscovery(numberOfCells): 201 | for x in range(numberOfCells): 202 | PublishDiscoverySub("sensor", "Cell_{}".format(x+1), "Cells.Cell_{}.Reading".format(x+1), "voltage", "V", "mdi:lightning-bolt") 203 | 204 | def publishDiscovery(pylonData): 205 | global current_pack_index 206 | 207 | PublishDiscoverySub("sensor", "PackVoltage", "PackVoltage.Reading", "voltage", "V", "mdi:lightning-bolt") 208 | PublishDiscoverySub("sensor", "PackCurrent", "PackCurrent.Reading", "current", "A", "mdi:current-dc") 209 | PublishDiscoverySub("sensor", "SOC", "SOC", "battery", "%", icon=0) 210 | PublishDiscoverySub("sensor", "RemainingCapacity", "RemainingCapacity", "current", "Ah", "mdi:ev-station") 211 | PublishCellsDiscovery(pylonData.NumberOfCells) 212 | PublishTempsDiscovery(pylonData.NumberOfTemperatures) 213 | discovery_published[current_pack_index] = True 214 | 215 | # --------------------------------------------------------------------------- # 216 | # Periodic will be called when needed. 217 | # If so, it will read from serial and publish to MQTT 218 | # --------------------------------------------------------------------------- # 219 | def periodic(polling_stop): 220 | 221 | global pylontech, infoPublished, currentPollRate, number_of_packs, current_pack_index, info_published, discovery_published, pack_barcodes, pack_versions 222 | 223 | if not polling_stop.is_set(): 224 | try: 225 | if mqttConnected: 226 | if pylontech is None: 227 | if argumentValues['sok']: 228 | pylontech = PylonTechSOK(argumentValues['pylonPort'], int(argumentValues['baud_rate'])) 229 | else: 230 | pylontech = Pylontech(argumentValues['pylonPort'], int(argumentValues['baud_rate'])) 231 | 232 | data = {} 233 | if number_of_packs == 0: 234 | number_of_packs = pylontech.get_pack_count().PackCount 235 | log.info("Pack count: {}".format(number_of_packs)) 236 | current_pack_index = 0 237 | info_published = [False] * number_of_packs 238 | discovery_published = [False] * number_of_packs 239 | pack_barcodes = [""] * number_of_packs 240 | pack_versions = [""] * number_of_packs 241 | 242 | else : 243 | current_pack_number = current_pack_index + 1 # pack number is origin 1 244 | if not info_published[current_pack_index]: 245 | vi = pylontech.get_version_info(current_pack_number) 246 | pack_versions[current_pack_index] = vi.Version 247 | log.info("version_info: {}".format(vi.Version)) 248 | if vi: 249 | bc = pylontech.get_barcode(current_pack_number) 250 | log.info("barcode: {}".format(bc.Barcode)) 251 | if bc: 252 | mqttPublish(encodePylon_info(vi, bc),"info/Pack{}".format(current_pack_number), True) 253 | info_published[current_pack_index] = True 254 | pack_barcodes[current_pack_index] = bc.Barcode 255 | pylonData = pylontech.get_values_single(current_pack_number) 256 | log.debug("get_values_single: {}".format(pylonData)) 257 | ai = pylontech.get_alarm_info(current_pack_number) 258 | log.debug("get_alarm_info: {}".format(ai)) 259 | if pylonData: # got data 260 | mqttPublish(encodePylon_readings(pylonData, ai),"readings/Pack{}".format(current_pack_number), False) 261 | if discovery_published[current_pack_index] == False: 262 | publishDiscovery(pylonData) 263 | 264 | else: 265 | log.error("PYLON data not good, skipping publish") 266 | current_pack_index += 1 267 | current_pack_index %= number_of_packs 268 | 269 | except Exception as e: 270 | log.error("Failed to process response!") 271 | log.exception(e, exc_info=True) 272 | if number_of_packs > 0: 273 | current_pack_index += 1 # move on to next pack 274 | current_pack_index %= number_of_packs 275 | 276 | timeUntilNextInterval = currentPollRate 277 | # set myself to be called again in correct number of seconds 278 | threading.Timer(timeUntilNextInterval, periodic, [polling_stop]).start() 279 | 280 | # --------------------------------------------------------------------------- # 281 | # Main 282 | # --------------------------------------------------------------------------- # 283 | def run(argv): 284 | 285 | global doStop, mqttClient, currentPollRate, log 286 | 287 | log.info("pylon_mqtt starting up...") 288 | 289 | handleArgs(argv, argumentValues) 290 | currentPollRate = argumentValues['publishRate'] 291 | 292 | #random seed from the OS 293 | seed(int.from_bytes( os.urandom(4), byteorder="big")) 294 | 295 | mqttErrorCount = 0 296 | 297 | #setup the MQTT Client for publishing and subscribing 298 | clientId = argumentValues['mqttUser'] + "_mqttclient_" + str(randint(100, 999)) 299 | log.info("Connecting with clientId=" + clientId) 300 | mqttClient = mqttclient.Client(clientId) 301 | mqttClient.username_pw_set(argumentValues['mqttUser'], password=argumentValues['mqttPassword']) 302 | mqttClient.on_connect = on_connect 303 | mqttClient.on_disconnect = on_disconnect 304 | mqttClient.on_message = on_message 305 | 306 | #Set Last Will 307 | will_topic = "{}{}/tele/LWT".format(argumentValues['mqttRoot'], argumentValues['rackName']) 308 | mqttClient.will_set(will_topic, payload="Offline", qos=0, retain=False) 309 | 310 | try: 311 | log.info("Connecting to MQTT {}:{}".format(argumentValues['mqttHost'], argumentValues['mqttPort'])) 312 | mqttClient.connect(host=argumentValues['mqttHost'],port=int(argumentValues['mqttPort'])) 313 | except Exception as e: 314 | log.error("Unable to connect to MQTT, exiting...") 315 | sys.exit(2) 316 | 317 | mqttClient.loop_start() 318 | 319 | #define the stop for the function 320 | periodic_stop = threading.Event() 321 | # start calling periodic now and every 322 | periodic(periodic_stop) 323 | 324 | log.debug("Starting main loop...") 325 | while not doStop: 326 | try: 327 | time.sleep(MAIN_LOOP_SLEEP_SECS) 328 | if not mqttConnected: 329 | if (mqttErrorCount > MQTT_MAX_ERROR_COUNT): 330 | log.error("MQTT Error count exceeded, disconnected, exiting...") 331 | doStop = True 332 | 333 | except KeyboardInterrupt: 334 | log.error("Got Keyboard Interuption, exiting...") 335 | doStop = True 336 | except Exception as e: 337 | log.error("Caught other exception...") 338 | log.exception(e, exc_info=True) 339 | 340 | log.info("Exited the main loop, stopping other loops") 341 | log.info("Stopping periodic async...") 342 | periodic_stop.set() 343 | 344 | log.info("Stopping MQTT loop...") 345 | mqttClient.loop_stop() 346 | 347 | log.info("Exiting pylon_mqtt") 348 | 349 | if __name__ == '__main__': 350 | number_of_packs = 0 351 | run(sys.argv[1:]) -------------------------------------------------------------------------------- /Code/Python/support/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Code/Python/support/__init__.py -------------------------------------------------------------------------------- /Code/Python/support/pylon_jsonencoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # --------------------------------------------------------------------------- # 4 | # Handle creating the Json packaging of the Pylon data. 5 | # 6 | # --------------------------------------------------------------------------- # 7 | 8 | 9 | import json 10 | import logging 11 | 12 | log = logging.getLogger("PylonToMQTT") 13 | 14 | # --------------------------------------------------------------------------- # 15 | # Handle creating the Json for Readings 16 | # --------------------------------------------------------------------------- # 17 | 18 | _tempNames = {0 : 'CellTemp1_4', 1 : 'CellTemp5_8', 2 : 'CellTemp9_12', 3 : 'CellTemp13_16', 4 : 'MOS_T', 5 : 'ENV_T' } 19 | 20 | def checkBit(var, pos): 21 | return (((var) & (1<<(pos))) != 0) 22 | 23 | def encodePylon_info(vi, bc): 24 | pylonData = {} 25 | pylonData["Version"] = vi.Version 26 | pylonData["BarCode"] = bc.Barcode 27 | return json.dumps(pylonData, sort_keys=False, separators=(',', ':')) 28 | 29 | def encodePylon_readings(decoded, ai): 30 | pylonData = {} 31 | cells = {} 32 | numberOfCells = decoded.NumberOfCells 33 | for c in range(numberOfCells): 34 | cellData = {} 35 | cellData["Reading"] = decoded.CellVoltages[c] 36 | if ai: # got alarm info? 37 | cellData["State"] = ai.CellState[c] 38 | key = "Cell_{}" 39 | cells[key.format(c+1)] = cellData 40 | pylonData["Cells"] = cells 41 | numberOfTemperatures = decoded.NumberOfTemperatures 42 | temperatures = {} 43 | for t in range(numberOfTemperatures): 44 | if t < 6: #protect lookup 45 | temperatureData = {} 46 | temperatureData["Reading"] = decoded.GroupedCellsTemperatures[t] 47 | if ai: # got alarm info? 48 | temperatureData["State"] = ai.CellsTemperatureStates[t] 49 | temperatures[_tempNames[t]] = temperatureData 50 | pylonData["Temps"] = temperatures 51 | 52 | current = {} 53 | current["Reading"] = decoded.Current 54 | if ai: # got alarm info? 55 | current["State"] = ai.CurrentState 56 | pylonData["PackCurrent"] = current 57 | 58 | voltage = {} 59 | voltage["Reading"] = decoded.Voltage 60 | if ai: # got alarm info? 61 | voltage["State"] = ai.VoltageState 62 | pylonData["PackVoltage"] = voltage 63 | 64 | pylonData["RemainingCapacity"] = decoded.RemainingCapacity 65 | pylonData["FullCapacity"] = decoded.TotalCapacity 66 | pylonData["CycleCount"] = decoded.CycleNumber 67 | pylonData["SOC"] = decoded.StateOfCharge 68 | pylonData["Power"] = decoded.Power 69 | if ai: # got alarm info? 70 | pso = {} 71 | ProtectSts1 = ai.ProtectSts1 72 | ProtectSts2 = ai.ProtectSts2 73 | pso["Charger_OVP"] = checkBit(ProtectSts1, 7) 74 | pso["SCP"] = checkBit(ProtectSts1, 6) 75 | pso["DSG_OCP"] = checkBit(ProtectSts1, 5) 76 | pso["CHG_OCP"] = checkBit(ProtectSts1, 4) 77 | pso["Pack_UVP"] = checkBit(ProtectSts1, 3) 78 | pso["Pack_OVP"] = checkBit(ProtectSts1, 2) 79 | pso["Cell_UVP"] = checkBit(ProtectSts1, 1) 80 | pso["Cell_OVP"] = checkBit(ProtectSts1, 0) 81 | pso["ENV_UTP"] = checkBit(ProtectSts2, 6) 82 | pso["ENV_OTP"] = checkBit(ProtectSts2, 5) 83 | pso["MOS_OTP"] = checkBit(ProtectSts2, 4) 84 | pso["DSG_UTP"] = checkBit(ProtectSts2, 3) 85 | pso["CHG_UTP"] = checkBit(ProtectSts2, 2) 86 | pso["DSG_OTP"] = checkBit(ProtectSts2, 1) 87 | pso["CHG_OTP"] = checkBit(ProtectSts2, 0) 88 | pylonData["Protect_Status"] = pso 89 | 90 | sso = {} 91 | SystemSts = ai.SystemSts 92 | sso["Fully_Charged"] = checkBit(ProtectSts2, 7) 93 | sso["Heater"] = checkBit(SystemSts, 7) 94 | sso["AC_in"] = checkBit(SystemSts, 5) 95 | sso["Discharge_MOS"] = checkBit(SystemSts, 2) 96 | sso["Charge_MOS"] = checkBit(SystemSts, 1) 97 | sso["Charge_Limit"] = checkBit(SystemSts, 0) 98 | pylonData["System_Status"] = sso 99 | 100 | fso = {} 101 | FaultSts = ai.FaultSts 102 | fso["Heater_Fault"] = checkBit(FaultSts, 7) 103 | fso["CCB_Fault"] = checkBit(FaultSts, 6) 104 | fso["Sampling_Fault"] = checkBit(FaultSts, 5) 105 | fso["Cell_Fault"] = checkBit(FaultSts, 4) 106 | fso["NTC_Fault"] = checkBit(FaultSts, 2) 107 | fso["DSG_MOS_Fault"] = checkBit(FaultSts, 1) 108 | fso["CHG_MOS_Fault"] = checkBit(FaultSts, 0) 109 | pylonData["Fault_Status"] = fso 110 | 111 | aso = {} 112 | AlarmSts1 = ai.AlarmSts1 113 | AlarmSts2 = ai.AlarmSts2 114 | aso["DSG_OC"] = checkBit(AlarmSts1, 5) 115 | aso["CHG_OC"] = checkBit(AlarmSts1, 4) 116 | aso["Pack_UV"] = checkBit(AlarmSts1, 3) 117 | aso["Pack_OV"] = checkBit(AlarmSts1, 2) 118 | aso["Cell_UV"] = checkBit(AlarmSts1, 1) 119 | aso["Cell_OV"] = checkBit(AlarmSts1, 0) 120 | 121 | aso["SOC_Low"] = checkBit(AlarmSts2, 7) 122 | aso["MOS_OT"] = checkBit(AlarmSts2, 6) 123 | aso["ENV_UT"] = checkBit(AlarmSts2, 5) 124 | aso["ENV_OT"] = checkBit(AlarmSts2, 4) 125 | aso["DSG_UT"] = checkBit(AlarmSts2, 3) 126 | aso["CHG_UT"] = checkBit(AlarmSts2, 2) 127 | aso["DSG_OT"] = checkBit(AlarmSts2, 1) 128 | aso["CHG_OT"] = checkBit(AlarmSts2, 0) 129 | pylonData["Alarm_Status"] = aso 130 | return json.dumps(pylonData, sort_keys=False, separators=(',', ':')) 131 | 132 | -------------------------------------------------------------------------------- /Code/Python/support/pylon_validate.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import logging 3 | import sys, getopt 4 | import re 5 | 6 | 7 | log = logging.getLogger("PylonToMQTT") 8 | 9 | def validateStrParameter(param, name, defaultValue): 10 | if isinstance(param, str): 11 | return param 12 | else: 13 | log.error("Invalid parameter, {} passed for {}".format(param, name)) 14 | return defaultValue 15 | 16 | 17 | def validateHostnameParameter(param, name, defaultValue): 18 | try: 19 | socket.gethostbyname(param) 20 | # It works -- use it. Prevents conflicts with 'invalid' configurations 21 | # that still work due to OS quirks 22 | return param 23 | except Exception as e: 24 | log.warning("Name resolution failed for {!r} passed for {}".format(param, name)) 25 | log.exception(e, exc_info=False) 26 | try: 27 | assert len(param) < 253 28 | # Permit name to end with a single dot. 29 | hostname = param[:-1] if param.endswith('.') else param 30 | # check each hostname segment. 31 | # '_': permissible in domain names, but not hostnames -- 32 | # however, many OSes permit them, so we permit them. 33 | allowed = re.compile("^(?!-)[A-Z\d_-]{1,63}(? --baud_rate <{}> --rack_name <{}> --mqtt_host <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --publish_rate <{}> --sok <{}>".format( \ 83 | argVals['pylonPort'], argVals['baud_rate'], argVals['rack_name'], argVals['mqttHost'], argVals['mqttPort'], argVals['mqttRoot'], argVals['publishRate'], argVals['sok'])) 84 | sys.exit(2) 85 | for opt, arg in opts: 86 | if opt == '-h': 87 | print ("Parameter help: py --pylon_port <{}> --baud_rate <{}> --rackName <{}> --mqtt_host <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --publish_rate <{}> --sok <{}>".format( \ 88 | argVals['pylonPort'], argVals['baud_rate'], argVals['rackName'], argVals['mqttHost'], argVals['mqttPort'], argVals['mqttRoot'], argVals['publishRate'], argVals['sok'])) 89 | sys.exit() 90 | elif opt in ('--pylon_port'): 91 | argVals['pylonPort'] = validateStrParameter(arg,"pylon_port", argVals['pylonPort']) 92 | elif opt in ('--baud_rate'): 93 | argVals['baud_rate'] = validateIntParameter(arg,"baud_rate", argVals['baud_rate']) 94 | elif opt in ('--rackName'): 95 | argVals['rackName'] = validateStrParameter(arg,"rack_name", argVals['rackName']) 96 | elif opt in ("--mqtt_host"): 97 | argVals['mqttHost'] = validateHostnameParameter(arg,"mqtt_host",argVals['mqttHost']) 98 | elif opt in ("--mqtt_port"): 99 | argVals['mqttPort'] = validateIntParameter(arg,"mqtt_port", argVals['mqttPort']) 100 | elif opt in ("--mqtt_root"): 101 | argVals['mqttRoot'] = validateStrParameter(arg,"mqtt_root", argVals['mqttRoot']) 102 | elif opt in ("--mqtt_user"): 103 | argVals['mqttUser'] = validateStrParameter(arg,"mqtt_user", argVals['mqttUser']) 104 | elif opt in ("--mqtt_pass"): 105 | argVals['mqttPassword'] = validateStrParameter(arg,"mqtt_pass", argVals['mqttPassword']) 106 | elif opt in ("--publish_rate"): 107 | argVals['publishRate'] = int(validateIntParameter(arg,"publish_rate", argVals['publishRate'])) 108 | elif opt in ("--sok"): 109 | argVals['sok'] = bool(validateBoolParamter(arg, "sok", argVals['sok'])) 110 | 111 | if ((argVals['publishRate'])MAX_PUBLISH_RATE): 115 | print("--publishRate must be less than or equal to {} seconds".format(MAX_PUBLISH_RATE)) 116 | sys.exit() 117 | 118 | argVals['rackName'] = argVals['rackName'].strip() 119 | argVals['pylonPort'] = argVals['pylonPort'].strip() 120 | 121 | argVals['mqttHost'] = argVals['mqttHost'].strip() 122 | argVals['mqttUser'] = argVals['mqttUser'].strip() 123 | 124 | log.info("pylonPort = {}".format(argVals['pylonPort'])) 125 | log.info("baud_rate = {}".format(argVals['baud_rate'])) 126 | log.info("rackName = {}".format(argVals['rackName'])) 127 | log.info("mqttHost = {}".format(argVals['mqttHost'])) 128 | log.info("mqttPort = {}".format(argVals['mqttPort'])) 129 | log.info("mqttRoot = {}".format(argVals['mqttRoot'])) 130 | log.info("mqttUser = {}".format(argVals['mqttUser'])) 131 | log.info("mqttPassword = **********") 132 | #log.info("mqttPassword = {}".format(argVals['mqttPassword'])) 133 | log.info("publishRate = {}".format(argVals['publishRate'])) 134 | log.info("sok = {}".format(argVals['sok'])) 135 | 136 | #Make sure the last character in the root is a "/" 137 | if (not argVals['mqttRoot'].endswith("/")): 138 | argVals['mqttRoot'] += "/" 139 | -------------------------------------------------------------------------------- /Code/Python/support/pylontech.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import serial 3 | import construct 4 | 5 | log = logging.getLogger("PylonToMQTT") 6 | 7 | class HexToByte(construct.Adapter): 8 | def _decode(self, obj, context, path) -> bytes: 9 | hexstr = ''.join([chr(x) for x in obj]) 10 | return bytes.fromhex(hexstr) 11 | 12 | class JoinBytes(construct.Adapter): 13 | def _decode(self, obj, context, path) -> bytes: 14 | return ''.join([chr(x) for x in obj]).encode() 15 | 16 | class DivideBy1000(construct.Adapter): 17 | def _decode(self, obj, context, path) -> float: 18 | return obj / 1000 19 | 20 | class DivideBy100(construct.Adapter): 21 | def _decode(self, obj, context, path) -> float: 22 | return obj / 100 23 | 24 | class ToVolt(construct.Adapter): 25 | def _decode(self, obj, context, path) -> float: 26 | return round((obj / 1000), 3) 27 | 28 | class ToAmp(construct.Adapter): 29 | def _decode(self, obj, context, path) -> float: 30 | return round((obj / 100), 2) 31 | 32 | class Round1(construct.Adapter): 33 | def _decode(self, obj, context, path) -> float: 34 | return round((obj), 1) 35 | 36 | class Round2(construct.Adapter): 37 | def _decode(self, obj, context, path) -> float: 38 | return round((obj), 2) 39 | 40 | class ToCelsius(construct.Adapter): 41 | def _decode(self, obj, context, path) -> float: 42 | return round(((obj - 2730) / 10.0), 2) # in Kelvin*10 43 | 44 | class Pylontech: 45 | 46 | get_alarm_fmt = construct.Struct( 47 | "NumberOfModule" / construct.Byte, 48 | "NumberOfCells" / construct.Int8ub, 49 | "CellState" / construct.Array(construct.this.NumberOfCells, construct.Int8ub), 50 | "NumberOfTemperatures" / construct.Int8ub, 51 | "CellsTemperatureStates" / construct.Array(construct.this.NumberOfTemperatures, construct.Int8ub), 52 | "_UserDefined1" / construct.Int8ub, 53 | "CurrentState" / construct.Int8ub, 54 | "VoltageState" / construct.Int8ub, 55 | "ProtectSts1" / construct.Int8ub, 56 | "ProtectSts2" / construct.Int8ub, 57 | "SystemSts" / construct.Int8ub, 58 | "FaultSts" / construct.Int8ub, 59 | "Skip81" / construct.Int16ub, 60 | "AlarmSts1" / construct.Int8ub, 61 | "AlarmSts2" / construct.Int8ub 62 | ) 63 | 64 | pack_count_fmt = construct.Struct( 65 | "PackCount" / construct.Byte, 66 | ) 67 | 68 | get_analog_fmt = construct.Struct( 69 | "NumberOfModule" / construct.Byte, 70 | "NumberOfCells" / construct.Int8ub, 71 | "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)), 72 | "NumberOfTemperatures" / construct.Int8ub, 73 | "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures, ToCelsius(construct.Int16sb)), 74 | "Current" / ToAmp(construct.Int16sb), 75 | "Voltage" / ToVolt(construct.Int16ub), 76 | "Power" / Round1(construct.Computed(construct.this.Current * construct.this.Voltage)), 77 | "_RemainingCapacity" / construct.Int16ub, 78 | "RemainingCapacity" / DivideBy100(construct.Computed(construct.this._RemainingCapacity)), 79 | "_UserDefinedItems" / construct.Int8ub, 80 | "TotalCapacity" / DivideBy100(construct.Int16ub), 81 | "CycleNumber" / construct.Int16ub, 82 | "StateOfCharge" / Round1(construct.Computed(construct.this._RemainingCapacity / construct.this.TotalCapacity)), 83 | ) 84 | 85 | def __init__(self, serial_port='/dev/ttyUSB0', baudrate=9600): 86 | self.s = serial.Serial(serial_port, baudrate, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=5) 87 | 88 | @staticmethod 89 | def get_frame_checksum(frame: bytes): 90 | assert isinstance(frame, bytes) 91 | sum = 0 92 | for byte in frame: 93 | sum += byte 94 | sum = ~sum 95 | sum %= 0x10000 96 | sum += 1 97 | return sum 98 | 99 | @staticmethod 100 | def get_info_length(info: bytes) -> int: 101 | lenid = len(info) 102 | if lenid == 0: 103 | return 0 104 | lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf) 105 | lenid_modulo = lenid_sum % 16 106 | lenid_invert_plus_one = 0b1111 - lenid_modulo + 1 107 | return (lenid_invert_plus_one << 12) + lenid 108 | 109 | def send_cmd(self, address: int, cmd, info: bytes = b''): 110 | raw_frame = self._encode_cmd(address, cmd, info) 111 | self.s.write(raw_frame) 112 | 113 | def _encode_cmd(self, address: int, cid2: int, info: bytes = b''): 114 | cid1 = 0x46 115 | info_length = Pylontech.get_info_length(info) 116 | frame = "{:02X}{:02X}{:02X}{:02X}{:04X}".format(0x20, address, cid1, cid2, info_length).encode() 117 | frame += info 118 | frame_chksum = Pylontech.get_frame_checksum(frame) 119 | whole_frame = (b"~" + frame + "{:04X}".format(frame_chksum).encode() + b"\r") 120 | return whole_frame 121 | 122 | def _decode_hw_frame(self, raw_frame: bytes) -> bytes: 123 | # XXX construct 124 | frame_data = raw_frame[1:len(raw_frame) - 5] 125 | frame_chksum = raw_frame[len(raw_frame) - 5:-1] 126 | got_frame_checksum = Pylontech.get_frame_checksum(frame_data) 127 | assert got_frame_checksum == int(frame_chksum, 16) 128 | return frame_data 129 | 130 | def _decode_frame(self, frame): 131 | format = construct.Struct( 132 | "ver" / HexToByte(construct.Array(2, construct.Byte)), 133 | "adr" / HexToByte(construct.Array(2, construct.Byte)), 134 | "cid1" / HexToByte(construct.Array(2, construct.Byte)), 135 | "cid2" / HexToByte(construct.Array(2, construct.Byte)), 136 | "infolength" / HexToByte(construct.Array(4, construct.Byte)), 137 | "info" / HexToByte(construct.GreedyRange(construct.Byte)), 138 | ) 139 | return format.parse(frame) 140 | 141 | def read_frame(self): 142 | raw_frame = self.s.readline() 143 | f = self._decode_hw_frame(raw_frame=raw_frame) 144 | parsed = self._decode_frame(f) 145 | return parsed 146 | 147 | def get_pack_count(self): 148 | self.send_cmd(0, 0x90) 149 | f = self.read_frame() 150 | return self.pack_count_fmt.parse(f.info) 151 | 152 | def get_version_info(self, dev_id): 153 | bdevid = "{:02X}".format(dev_id).encode() 154 | self.send_cmd(0, 0xC1, bdevid) 155 | f = self.read_frame() 156 | version_info_fmt = construct.Struct( 157 | "Version" / construct.CString("utf8") 158 | ) 159 | return version_info_fmt.parse(f.info) 160 | 161 | def get_barcode(self, dev_id): 162 | bdevid = "{:02X}".format(dev_id).encode() 163 | self.send_cmd(0, 0xC2, bdevid) 164 | f = self.read_frame() 165 | version_info_fmt = construct.Struct( 166 | "Barcode" / construct.PaddedString(15, "utf8") 167 | ) 168 | return version_info_fmt.parse(f.info) 169 | 170 | def get_alarm_info(self, dev_id): 171 | bdevid = "{:02X}".format(dev_id).encode() 172 | self.send_cmd(dev_id, 0x44, bdevid) 173 | f = self.read_frame() 174 | il = int.from_bytes(f.infolength, byteorder='big', signed=False) 175 | il &= 0x0FFF 176 | log.debug("get_alarm_info infolength: {}".format(il)) 177 | if il > 22: # minimum response size 178 | return self.get_alarm_fmt.parse(f.info[1:]) 179 | else: 180 | return 181 | 182 | def get_values_single(self, dev_id): 183 | bdevid = "{:02X}".format(dev_id).encode() 184 | self.send_cmd(dev_id, 0x42, bdevid) 185 | f = self.read_frame() 186 | il = int.from_bytes(f.infolength, byteorder='big', signed=False) 187 | il &= 0x0FFF 188 | log.debug("get_values_single infolength: {}".format(il)) 189 | if il > 45: # minimum response size 190 | return self.get_analog_fmt.parse(f.info[1:]) 191 | else: 192 | return 193 | 194 | class PylonTechSOK(Pylontech): 195 | 196 | # SOK 48v BMS uses RS232 protocol v.2.5 (0x25) 197 | def _encode_cmd(self, address: int, cid2: int, info: bytes = b''): 198 | cid1 = 0x46 199 | info_length = Pylontech.get_info_length(info) 200 | frame = "{:02X}{:02X}{:02X}{:02X}{:04X}".format(0x25, address, cid1, cid2, info_length).encode() 201 | frame += info 202 | frame_chksum = Pylontech.get_frame_checksum(frame) 203 | whole_frame = (b"~" + frame + "{:04X}".format(frame_chksum).encode() + b"\r") 204 | return whole_frame 205 | 206 | # SOK 48v BMS reports 20 char version string padded with spaces (0x20) 207 | def get_version_info(self, dev_id): 208 | bdevid = "{:02X}".format(dev_id).encode() 209 | self.send_cmd(0, 0xC1, bdevid) 210 | f = self.read_frame() 211 | version_info_fmt = construct.Struct( 212 | "Version" / construct.PaddedString(20, "utf8") 213 | ) 214 | return version_info_fmt.parse(f.info) -------------------------------------------------------------------------------- /Code/Python/telegraf.conf: -------------------------------------------------------------------------------- 1 | [global_tags] 2 | [agent] 3 | interval = "10s" 4 | round_interval = true 5 | metric_batch_size = 1000 6 | metric_buffer_limit = 10000 7 | collection_jitter = "0s" 8 | flush_interval = "10s" 9 | flush_jitter = "0s" 10 | precision = "" 11 | hostname = "" 12 | omit_hostname = false 13 | 14 | ############################################################################### 15 | # OUTPUT PLUGINS # 16 | ############################################################################### 17 | 18 | [[outputs.influxdb]] 19 | urls = ["http://influxdb:8086"] 20 | database = "mqtt_pylon" 21 | timeout = "5s" 22 | username = "dba" 23 | password = "supremo" 24 | 25 | ############################################################################### 26 | # SERVICE INPUT PLUGINS # 27 | ############################################################################### 28 | 29 | # Read metrics from MQTT topic(s) 30 | [[inputs.mqtt_consumer]] 31 | servers = ["tcp://mosquitto:1883"] 32 | ## MQTT QoS, must be 0, 1, or 2 33 | qos = 0 34 | 35 | ## Topics to subscribe to 36 | topics = [ 37 | "PylonToMQTT/Main/stat/readings/Pack1", 38 | "PylonToMQTT/Main/stat/readings/Pack2", 39 | "PylonToMQTT/Main/stat/readings/Pack3", 40 | "PylonToMQTT/Main/stat/readings/Pack4", 41 | "PylonToMQTT/Main/stat/readings/Pack5", 42 | "PylonToMQTT/Main/stat/readings/Pack6", 43 | "PylonToMQTT/Main/stat/readings/Pack7", 44 | "PylonToMQTT/Main/stat/readings/Pack8" 45 | ] 46 | 47 | # if true, messages that can't be delivered while the subscriber is offline 48 | # will be delivered when it comes back (such as on service restart). 49 | # NOTE: if true, client_id MUST be set 50 | persistent_session = false 51 | # If empty, a random client ID will be generated. 52 | client_id = "influxDB" 53 | 54 | ## username and password to connect MQTT server. 55 | #username = "ClassicPublisher" 56 | #password = "ClassicPub123" 57 | 58 | ## Data format to consume. 59 | ## Each data format has it's own unique set of configuration options, read 60 | ## more about them here: 61 | ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md 62 | data_format = "json" 63 | 64 | 65 | -------------------------------------------------------------------------------- /Code/node-red/P16100A-Simulator.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "8d931bf7e831a10d", 4 | "type": "tab", 5 | "label": "Flow 6", 6 | "disabled": false, 7 | "info": "", 8 | "env": [] 9 | }, 10 | { 11 | "id": "23576b832ef59022", 12 | "type": "serial in", 13 | "z": "8d931bf7e831a10d", 14 | "name": "", 15 | "serial": "e1b98ee07cee8803", 16 | "x": 110, 17 | "y": 180, 18 | "wires": [ 19 | [ 20 | "a22b3457a1dc78b9", 21 | "25ec8cace1753c19" 22 | ] 23 | ] 24 | }, 25 | { 26 | "id": "a22b3457a1dc78b9", 27 | "type": "debug", 28 | "z": "8d931bf7e831a10d", 29 | "name": "RX", 30 | "active": true, 31 | "tosidebar": true, 32 | "console": false, 33 | "tostatus": false, 34 | "complete": "payload", 35 | "targetType": "msg", 36 | "statusVal": "", 37 | "statusType": "auto", 38 | "x": 350, 39 | "y": 100, 40 | "wires": [] 41 | }, 42 | { 43 | "id": "25ec8cace1753c19", 44 | "type": "function", 45 | "z": "8d931bf7e831a10d", 46 | "name": "function 1", 47 | "func": "var input = {};\ninput = msg.payload;\n\nif (input[0] == '~') {\n var addr = input[3] + input[4]\n var cmd = input[5] + input[6] + input[7] + input[8]\n var cell1 = getRandomInt(2800, 3600);\n var hcell1 = decimalToHex(cell1, 4);\n switch (cmd) {\n case '4690':\n msg.payload = `25${addr}4600E00203`;\n break;\n case '46C1':\n msg.payload = `25${addr}4600602850313653313030412D31423437302D312E303400`;\n break;\n case '46C2':\n msg.payload = `25${addr}4600B050314234373031303230313032333844202020202044656320203720323032312C31373A35323A3338`;\n break;\n case '4642':\n msg.payload = `25${addr}4600D07C00${addr}10${hcell1}0D240D240D210D210D220D230D240D220D210D220D220D230D220D230D21060B230B210B270B260B450B4E0050D22327CF0227D6000B271063`;\n break;\n case '4644':\n msg.payload = `25${addr}4600E04E00${addr}10020202020101010000000000000000010602010101010100000000000000000000000000`;\n break;\n }\n return msg;\n} \n\nfunction getRandomInt(min, max) {\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nfunction decimalToHex(d, padding) {\n var hex = Number(d).toString(16);\n padding = typeof (padding) === \"undefined\" || padding === null ? padding = 2 : padding;\n\n while (hex.length < padding) {\n hex = \"0\" + hex;\n }\n\n return hex;\n}", 48 | "outputs": 1, 49 | "noerr": 0, 50 | "initialize": "", 51 | "finalize": "", 52 | "libs": [], 53 | "x": 360, 54 | "y": 180, 55 | "wires": [ 56 | [ 57 | "df727f6b2506240d" 58 | ] 59 | ] 60 | }, 61 | { 62 | "id": "92b23b714a49fd56", 63 | "type": "debug", 64 | "z": "8d931bf7e831a10d", 65 | "name": "Trace", 66 | "active": true, 67 | "tosidebar": true, 68 | "console": false, 69 | "tostatus": false, 70 | "complete": "payload", 71 | "targetType": "msg", 72 | "statusVal": "", 73 | "statusType": "auto", 74 | "x": 870, 75 | "y": 120, 76 | "wires": [] 77 | }, 78 | { 79 | "id": "bdf7117ff228d20a", 80 | "type": "serial out", 81 | "z": "8d931bf7e831a10d", 82 | "name": "COM9", 83 | "serial": "e1b98ee07cee8803", 84 | "x": 870, 85 | "y": 180, 86 | "wires": [] 87 | }, 88 | { 89 | "id": "df727f6b2506240d", 90 | "type": "function", 91 | "z": "8d931bf7e831a10d", 92 | "name": "add checksum", 93 | "func": "function checksum(s) {\n var sum = 0;\n var len = s.length;\n for (var i = 0; i < len; i++) {\n sum += (s.charCodeAt(i));\n }\n sum = ~sum + 1;\n return (sum & 0xffff).toString(16).toUpperCase();\n}\n\n\nmsg.payload = \"~\" + msg.payload + checksum(msg.payload) + \"\\r\";\n\nreturn msg;", 94 | "outputs": 1, 95 | "noerr": 0, 96 | "initialize": "", 97 | "finalize": "", 98 | "libs": [], 99 | "x": 640, 100 | "y": 180, 101 | "wires": [ 102 | [ 103 | "92b23b714a49fd56", 104 | "bdf7117ff228d20a" 105 | ] 106 | ] 107 | }, 108 | { 109 | "id": "e1b98ee07cee8803", 110 | "type": "serial-port", 111 | "serialport": "COM9", 112 | "serialbaud": "9600", 113 | "databits": "8", 114 | "parity": "none", 115 | "stopbits": "1", 116 | "waitfor": "~", 117 | "dtr": "none", 118 | "rts": "none", 119 | "cts": "none", 120 | "dsr": "none", 121 | "newline": "\\r", 122 | "bin": "false", 123 | "out": "char", 124 | "addchar": "", 125 | "responsetimeout": "10000" 126 | } 127 | ] -------------------------------------------------------------------------------- /Code/node-red/flows.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "25ec8cace1753c19", 4 | "type": "function", 5 | "z": "8d931bf7e831a10d", 6 | "name": "function 1", 7 | "func": "var input = {};\ninput = msg.payload;\nconst barcodes = [\n \"314234373031303230313032333844\",\n \"314234373031303230313032333845\",\n \"314234373031303230313032333846\",\n];\n\n\nif (input[0] == '~') {\n let index = context.get('index') || 0\n index = (index + 1) % barcodes.length\n context.set('index', index);\n var addr = input[3] + input[4]\n var cmd = input[5] + input[6] + input[7] + input[8]\n var cell1 = getRandomInt(2800, 3600);\n var hcell1 = decimalToHex(cell1, 4);\n switch (cmd) {\n case '4690':\n msg.payload = `25${addr}4600E00203`;\n break;\n case '46C1':\n msg.payload = `25${addr}4600602850313653313030412D31423437302D312E303400`;\n break;\n case '46C2':\n msg.payload = `25${addr}4600B050${barcodes[index]}202020202044656420203720323032312C31373A35323A3338`;\n break;\n case '4642':\n msg.payload = `25${addr}4600D07C00${addr}10${hcell1}0D260D240D210D210D220D230D240D220D210D220D220D230D220D230D21060B230B210B270B260B450B4E0050D23307CF0221C6000B271063`;\n break;\n case '4644':\n msg.payload = `25${addr}4600E04E00${addr}10020202020101010000000000000000010602010101010100000000000000000000000000`;\n break;\n }\n return msg;\n} \n\nfunction getRandomInt(min, max) {\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nfunction decimalToHex(d, padding) {\n var hex = Number(d).toString(16);\n padding = typeof (padding) === \"undefined\" || padding === null ? padding = 2 : padding;\n\n while (hex.length < padding) {\n hex = \"0\" + hex;\n }\n\n return hex;\n}", 8 | "outputs": 1, 9 | "timeout": "", 10 | "noerr": 0, 11 | "initialize": "", 12 | "finalize": "", 13 | "libs": [], 14 | "x": 360, 15 | "y": 180, 16 | "wires": [ 17 | [ 18 | "df727f6b2506240d" 19 | ] 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /Code/node-red/notes.txt: -------------------------------------------------------------------------------- 1 | Install node-red on Windows 11 2 | 3 | 1. Install Node.js 4 | https://nodejs.org/en/download 5 | Download and install node .msi 6 | 7 | 2. Check node version in cmd window 8 | node --version && npm --version 9 | 10 | 3. Install node-red 11 | npm install -g --unsafe-perm node-red 12 | 13 | 4. Run node-red 14 | node-red 15 | 16 | 5. Open node-red page at http://localhost:1880/ 17 | 18 | 6. Using "Manage Palette" - Install node-red-node-serialport in node-red 19 | 20 | 7. Import P16100A-Simulator.json 21 | 22 | 8. Configure COM port in serial-port node 23 | -------------------------------------------------------------------------------- /Docs/GetAlarmInfo.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | GetAlarmInfo CID2:0x44 4 | Sample with 16 cells, 6 temps 5 | 6 | 7 | 8 | 9 | 1 10 | 1 2 3 4 5 6 7 8 9 0 11 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 12 | 25014600E04E00011002020202010101000000000000000001060201010101020002020080A600000000000000 13 | 25014600E04E00011002020202010101000000000000000001060201010101020002020080A700000000000000 14 | 17: Number of cells 15 | 19: Cell 1: below lower limit - 0x01, above higher limit - 0x02 16 | 21 17 | 23 18 | 25 19 | 27 20 | 29 21 | 31 22 | 33 23 | 35 24 | 37 25 | 39 26 | 41 27 | 43 28 | 45 29 | 47: 30 | 49: Cell 16: below lower limit - 0x01, above higher limit - 0x02 31 | 51: Number of Temps 32 | 53: Tcell 1 (Cell temperature 1~4): below lower limit - 0x01, above higher limit - 0x02 33 | 55: Tcell 2 (Cell temperature 5~8): 34 | 57: Tcell 3 (Cell temperature 9~12): 35 | 59: Tcell 4 (Cell temperature 13~16): 36 | 61: MOS_T (MOSFET temperature): 37 | 63: ENV_T: (Environment temperature) 38 | 65: 00 39 | 67: Pack Voltage 01 02 40 | 69: Pack Current 01 02 41 | 71 Protect Status: Charger OVP 0x80, SCP 0x40, DSG OCP 0x20, CHG OCP 0x10, Pack UVP 0x08, Pack OVP 0x04, Cell UVP 0x02, Cell OVP 0x01 42 | 73: Fully (0x80) - Protect Status: (ENV UTP 0x40, ENV OTP 0x20, MOS OTP 0x10, DSG UTP 0x08, CHG UTP 0x04, DSG OTP 0x02, CHG OTP 0x01) 43 | 75 System Status: htr-on 0x80, 0x40 , AC in 0x20, 0x10 , 0x08 , DSG-MOS-ON 0x04, CHG-MOS-ON 0x02, CHG-LIMIT-ON 0x01 44 | 77: 00 45 | 79 Fault Status: (Heater Fault 0x80 , CCB Fault 0x40, Sampling Fault 0x20, Cell Fault 0x10, NTC Fault 0x04, DSG MOS Fault 0x02, CHG MOS Fault 0x01) 46 | 81 47 | 83 48 | 85 Alarm status: (DSG OC 0x20, CHG OC 0x10, Pack UV 0x08, Pack OV 0x04, Cell UV 0x02, Cell OV 0x01) 49 | 87 Alarm status: (SOC Low 0x80, MOS OT 0x40, ENV UT 0x20, ENV OT 0x10, DSG UT 0x08, CHG UT 0x04, DSG OT 0x02, CHG OT 0x01 50 | 89 00 51 | 52 | 53 | ProtectSts1: 00, ProtectSts2: 80, SystemSts: A7, FaultSts: 00, AlarmSts1: 00, AlarmSts2: 00 -------------------------------------------------------------------------------- /Docs/GetAnalogValue.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | GetAnalogValue(FF) 4 | ~25FF4642E00201FD05 5 | ~25014600D07C0001100D200D240D240D210D210D220D230D240D220D210D220D220D230D220D230D21060B230B210B270B260B450B4E0050D22327CF0227D6000B271063 E372 6 | 7 | 8 | 1 9 | 1 2 3 4 5 6 7 8 9 0 1 2 3 4 10 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 11 | 25014600D07C0001100D200D240D240D210D210D220D230D240D220D210D220D220D230D220D230D21060B230B210B270B260B450B4E0050D22327CF0227D6000B271063 12 | 13 | 17: Number of cells 14 | 19: Cell 1: volage (mv) 15 | 23 16 | 27 17 | 31 18 | 35 19 | 39 20 | 43 21 | 47 22 | 51 23 | 55 24 | 59 25 | 63 26 | 67 27 | 71 28 | 75 29 | 79: Cell 16: volage (mv) 30 | 83: Number of Temps 31 | 85: Tcell 1 (Cell temperature 1~4): Celsius /100 32 | 89: Tcell 2 (Cell temperature 5~8): 33 | 93: Tcell 3 (Cell temperature 9~12): 34 | 97: Tcell 4 (Cell temperature 13~16): 35 | 101: MOS_T (MOSFET temperature): 36 | 105: ENV_T: (Environment temperature) 37 | 109: Pack Current mA 38 | 113: Pack Voltage mv 39 | 117: Remaining Capacity 40 | 121: User Define Code: 02 41 | 123: Full Capacity mAh { SOC: (Remaining Capacity * 100) / Full Capacity } 42 | 127: Cycle Count: 0~65535 43 | 131: 271063 ??? -------------------------------------------------------------------------------- /Docs/GetBarCode.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | GetBarCode 4 | CID2: 0xC2 5 | 6 | ~25FF46C20000FD6E 7 | ~25014600B050314234373031303230313032333844202020202044656320203720323032312C31373A35323A3338 ED95 8 | 9 | 10 | 1 11 | 1 2 3 4 5 6 7 8 9 0 12 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 13 | 25014600B050314234373031303230313032333844202020202044656320203720323032312C31373A35323A3338 14 | 15 | 13: 1B470102010238D 16 | 53: Dec 7 2021,17:52:38 17 | 18 | ~000346C10000FD9F 19 | ~000346C20000FD9E 20 | 250246C2E00202FD1F 21 | ~00034642E00203FD33 22 | ~00034644E00203FD31 23 | 24 | ~000146C10000FDA1 25 | ~000146C20000FDA0 26 | ~00014642E00201FD37 27 | ~00014644E00201FD35 28 | 29 | -------------------------------------------------------------------------------- /Docs/GetBarCodes_Trace.log: -------------------------------------------------------------------------------- 1 | GetBarCodes_Trace 2 | 3 | 250146C2E00201FD21 4 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 5 | 6 | 250246C2E00202FD1F 7 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 8 | 9 | 250346C2E00203FD1D 10 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 11 | 12 | 250446C2E00204FD1B 13 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 14 | 15 | 250546C2E00205FD19 16 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 17 | 18 | 19 | 000246C10000FDA0 20 | 000246C10000FDA0 21 | 22 | var input = {}; 23 | input = msg.payload; 24 | const barcodes = [ 25 | "314234373031303230313032333844", 26 | "314234373031303230313032333845", 27 | "314234373031303230313032333846", 28 | ]; 29 | 30 | 31 | if (input[0] == '~') { 32 | let index = context.get('index') || 0 33 | index = (index + 1) % barcodes.length 34 | context.set('index', index); 35 | var addr = input[3] + input[4] 36 | var cmd = input[5] + input[6] + input[7] + input[8] 37 | var cell1 = getRandomInt(2800, 3600); 38 | var hcell1 = decimalToHex(cell1, 4); 39 | switch (cmd) { 40 | case '4690': 41 | msg.payload = `25${addr}4600E00203`; 42 | break; 43 | case '46C1': 44 | msg.payload = `25${addr}4600602850313653313030412D31423437302D312E303400`; 45 | break; 46 | case '46C2': 47 | msg.payload = `25${addr}4600B050${barcodes[index]}202020202044656420203720323032312C31373A35323A3338`; 48 | break; 49 | case '4642': 50 | msg.payload = `25014600D07C00${addr}10${hcell1}0D260D240D210D210D220D230D240D220D210D220D220D230D220D230D21060B230B210B270B260B450B4E0050D23307CF0221C6000B271063`; 51 | 52 | break; 53 | case '4644': 54 | msg.payload = `25014600E04E00${addr}10020202020101010000000000000000010602010101010100000000000000000000000000`; 55 | break; 56 | } 57 | return msg; 58 | } 59 | -------------------------------------------------------------------------------- /Docs/GetBarCodes_Trace.txt: -------------------------------------------------------------------------------- 1 | GetBarCodes_Trace 2 | 3 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 4 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 5 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 6 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 7 | 25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 -------------------------------------------------------------------------------- /Docs/GetParameterSettings.txt: -------------------------------------------------------------------------------- 1 | Get Parameter settings 2 | 3 | GetCellOVParams (D1) 4 | ~000046D10000FDA1 5 | ~2501 4600 F010 010E100E740D5C0A FA23 6 | 7 | GetCPackOVParams (D5) 8 | ~000046D50000FD9D 9 | ~2501 4600 F010 01E100E420D2F00A FA28 10 | 11 | GetCCellUVParams (D3) 12 | ~000046D30000 FD9D 13 | ~2501 4600 F010 01E100E420D2F00A FA28 14 | 15 | GetCPackUVParams (D7) 16 | ~000046D70000 FD9D 17 | ~2501 4600 F010 01E100E420D2F00A FA28 18 | 19 | GetChargeOCParams (D9) 20 | ~000046D90000 FD9D 21 | ~2501 4600 F010 00 0000 0101 64 FA28 22 | 23 | GetDischargeOCParams (DB) 24 | ~000046DB0000 FD9D 25 | ~2501 4600 F010 01E100E420D2F00A FA28 -------------------------------------------------------------------------------- /Docs/GetVersionInfo.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | GetVersionInfo 4 | CID2: 0xC1 5 | 6 | ~25FF46C10000FD6F 7 | ~25014600602850313653313030412D31423437302D312E303400 F586 8 | 9 | 10 | 1 11 | 1 2 3 4 5 6 7 8 9 0 12 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 13 | 25014600602850313653313030412D31423437302D312E303400 14 | 15 | 13: P16S100A-1B470-1.04 16 | 17 | -------------------------------------------------------------------------------- /Docs/PACE 16S 100Ah.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Docs/PACE 16S 100Ah.pdf -------------------------------------------------------------------------------- /Docs/SampleMQTT.json: -------------------------------------------------------------------------------- 1 | Topic: Pylontech/Rack1/stat/readings/pack-1 2 | 3 | 4 | { 5 | "Version" : "P16S100A-1B470-1.04", 6 | "BarCode" : "1B470102010238D Dec 7 2021,17:52:38", 7 | "Cells" : { 8 | "Cell-1" : { 9 | "Reading" : 3.584, 10 | "State" : 2 11 | }, 12 | "Cell-2" : { 13 | "Reading" : 3.364, 14 | "State" : 2 15 | }, 16 | "Cell-3" : { 17 | "Reading" : 3.364, 18 | "State" : 2 19 | }, 20 | "Cell-4" : { 21 | "Reading" : 3.361, 22 | "State" : 2 23 | }, 24 | "Cell-5" : { 25 | "Reading" : 3.361, 26 | "State" : 1 27 | }, 28 | "Cell-6" : { 29 | "Reading" : 3.362, 30 | "State" : 1 31 | }, 32 | "Cell-7" : { 33 | "Reading" : 3.363, 34 | "State" : 1 35 | }, 36 | "Cell-8" : { 37 | "Reading" : 3.364, 38 | "State" : 0 39 | }, 40 | "Cell-9" : { 41 | "Reading" : 3.362, 42 | "State" : 0 43 | }, 44 | "Cell-10" : { 45 | "Reading" : 3.361, 46 | "State" : 0 47 | }, 48 | "Cell-11" : { 49 | "Reading" : 3.362, 50 | "State" : 0 51 | }, 52 | "Cell-12" : { 53 | "Reading" : 3.362, 54 | "State" : 0 55 | }, 56 | "Cell-13" : { 57 | "Reading" : 3.363, 58 | "State" : 0 59 | }, 60 | "Cell-14" : { 61 | "Reading" : 3.362, 62 | "State" : 0 63 | }, 64 | "Cell-15" : { 65 | "Reading" : 3.363, 66 | "State" : 0 67 | }, 68 | "Cell-16" : { 69 | "Reading" : 3.361, 70 | "State" : 1 71 | } 72 | }, 73 | "Temps" : { 74 | "CellTemp1~4" : { 75 | "Reading" : 28.51, 76 | "State" : 2 77 | }, 78 | "CellTemp5~8" : { 79 | "Reading" : 28.49, 80 | "State" : 1 81 | }, 82 | "CellTemp9~12" : { 83 | "Reading" : 28.55, 84 | "State" : 1 85 | }, 86 | "CellTemp13~16" : { 87 | "Reading" : 28.54, 88 | "State" : 1 89 | }, 90 | "MOS_T" : { 91 | "Reading" : 28.85, 92 | "State" : 1 93 | }, 94 | "ENV_T" : { 95 | "Reading" : 28.94, 96 | "State" : 1 97 | } 98 | }, 99 | "PackCurrent" : { 100 | "Reading" : 0.08, 101 | "State" : 1 102 | }, 103 | "PackVoltage" : { 104 | "Reading" : 53.795, 105 | "State" : 2 106 | }, 107 | "RemainingCapacity" : 101.91, 108 | "FullCapacity" : 101.98, 109 | "CycleCount" : 11, 110 | "SOC" : 99, 111 | "Protect Status" : { 112 | "Charger OVP" : 0, 113 | "SCP" : 0, 114 | "DSG OCP" : 0, 115 | "CHG OCP" : 0, 116 | "Pack UVP" : 0, 117 | "Pack OVP" : 0, 118 | "Cell UVP" : 0, 119 | "Cell OVP" : 0, 120 | "ENV UTP" : 0, 121 | "ENV OTP" : 0, 122 | "MOS OTP" : 0, 123 | "DSG UTP" : 0, 124 | "CHG UTP" : 0, 125 | "DSG OTP" : 0, 126 | "CHG OTP" : 0 127 | }, 128 | "System Status" : { 129 | "Fully Charged" : 128, 130 | "Heater" : 0, 131 | "AC in" : 0, 132 | "Discharge-MOS" : 0, 133 | "Charge-MOS" : 0, 134 | "Charge-Limit" : 0 135 | }, 136 | "Fault Status" : { 137 | "Heater Fault" : 0, 138 | "CCB Fault" : 0, 139 | "Sampling Fault" : 0, 140 | "Cell Fault" : 0, 141 | "NTC Fault" : 0, 142 | "DSG MOS Fault" : 0, 143 | "CHG MOS Fault" : 0 144 | }, 145 | "Alarm Status" : { 146 | "DSG OC" : 0, 147 | "CHG OC" : 0, 148 | "Pack UV" : 0, 149 | "Pack OV" : 0, 150 | "Cell UV" : 0, 151 | "Cell OV" : 0, 152 | "SOC Low" : 0, 153 | "MOS OT" : 0, 154 | "ENV UT" : 0, 155 | "ENV OT" : 0, 156 | "DSG UT" : 0, 157 | "CHG UT" : 0, 158 | "DSG OT" : 0, 159 | "CHG OT" : 0 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Docs/Traces.txt: -------------------------------------------------------------------------------- 1 | [ 33846][I][WebLog.cpp:51] operator()(): [0] Web Log Connected! 2 | 3 | [ 35669][D][IOT.cpp:84] operator()(): Connecting to MQTT... 4 | [ 35695][I][IOT.cpp:131] operator()(): Connected to MQTT. Session present: 0 5 | [ 35698][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25FF4690E002FFFCD7 6 | 7 | [ 35701][D][Pylon.cpp:55] onMqttConnect(): onMqttConnect 8 | [ 35752][D][Pylon.cpp:260] ParseResponse(): received: 19 9 | [ 35755][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E00205FD32 10 | [ 35758][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 02 (2), CHKSUM: FD32 11 | [ 35765][I][Pylon.cpp:478] ParseResponse(): GetPackCount: 5 12 | [ 36209][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250146C1E00201FD22 13 | 14 | [ 36305][D][Pylon.cpp:260] ParseResponse(): received: 57 15 | [ 36308][D][Pylon.cpp:261] ParseResponse(): data: ~25014600602850313653313030412D31423437302D312E303400F586 16 | [ 36311][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 28 (40), CHKSUM: F586 17 | [ 36714][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250146C2E00201FD21 18 | 19 | [ 36857][D][Pylon.cpp:260] ParseResponse(): received: 97 20 | [ 36862][D][Pylon.cpp:261] ParseResponse(): data: ~25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 21 | [ 36869][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 50 (80), CHKSUM: ED77 22 | [ 36879][I][Pylon.cpp:464] ParseResponse(): GetBarCode for 1 bc: 1B470102170999D Mar 30 2022,18:18:16 23 | [ 37727][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250246C1E00202FD20 24 | 25 | [ 37825][D][Pylon.cpp:260] ParseResponse(): received: 57 26 | [ 37828][D][Pylon.cpp:261] ParseResponse(): data: ~25014600602850313653313030412D31423437302D312E303400F586 27 | [ 37831][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 28 (40), CHKSUM: F586 28 | [ 38234][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250246C2E00202FD1F 29 | 30 | [ 38377][D][Pylon.cpp:260] ParseResponse(): received: 97 31 | [ 38383][D][Pylon.cpp:261] ParseResponse(): data: ~25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 32 | [ 38391][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 50 (80), CHKSUM: ED77 33 | [ 38401][I][Pylon.cpp:464] ParseResponse(): GetBarCode for 1 bc: 1B470102170999D Mar 30 2022,18:18:16 34 | [ 39247][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250346C1E00203FD1E 35 | 36 | [ 39344][D][Pylon.cpp:260] ParseResponse(): received: 57 37 | [ 39349][D][Pylon.cpp:261] ParseResponse(): data: ~25014600602850313653313030412D31423437302D312E303400F586 38 | [ 39354][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 28 (40), CHKSUM: F586 39 | [ 39754][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250346C2E00203FD1D 40 | 41 | [ 39897][D][Pylon.cpp:260] ParseResponse(): received: 97 42 | [ 39903][D][Pylon.cpp:261] ParseResponse(): data: ~25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 43 | [ 39910][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 50 (80), CHKSUM: ED77 44 | [ 39919][I][Pylon.cpp:464] ParseResponse(): GetBarCode for 1 bc: 1B470102170999D Mar 30 2022,18:18:16 45 | [ 40767][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250446C1E00204FD1C 46 | 47 | [ 40864][D][Pylon.cpp:260] ParseResponse(): received: 57 48 | [ 40868][D][Pylon.cpp:261] ParseResponse(): data: ~25014600602850313653313030412D31423437302D312E303400F586 49 | [ 40871][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 28 (40), CHKSUM: F586 50 | [ 41274][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250446C2E00204FD1B 51 | 52 | [ 41417][D][Pylon.cpp:260] ParseResponse(): received: 97 53 | [ 41421][D][Pylon.cpp:261] ParseResponse(): data: ~25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 54 | [ 41428][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 50 (80), CHKSUM: ED77 55 | [ 41438][I][Pylon.cpp:464] ParseResponse(): GetBarCode for 1 bc: 1B470102170999D Mar 30 2022,18:18:16 56 | [ 42288][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250546C1E00205FD1A 57 | 58 | [ 42385][D][Pylon.cpp:260] ParseResponse(): received: 57 59 | [ 42389][D][Pylon.cpp:261] ParseResponse(): data: ~25014600602850313653313030412D31423437302D312E303400F586 60 | [ 42392][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 28 (40), CHKSUM: F586 61 | [ 42795][D][Pylon.cpp:218] send_cmd(): send_cmd: ~250546C2E00205FD19 62 | 63 | [ 42938][D][Pylon.cpp:260] ParseResponse(): received: 97 64 | [ 42941][D][Pylon.cpp:261] ParseResponse(): data: ~25014600B05031423437303130323137303939394420202020204D617220333020323032322C31383A31383A3136ED77 65 | [ 42948][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 50 (80), CHKSUM: ED77 66 | [ 42958][I][Pylon.cpp:464] ParseResponse(): GetBarCode for 1 bc: 1B470102170999D Mar 30 2022,18:18:16 67 | [ 48308][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014642E00201FD30 68 | 69 | [ 48502][D][Pylon.cpp:260] ParseResponse(): received: 141 70 | [ 48507][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0001100D8B0D280D2A0D290D2C0D260D290D2B0D2B0D5A0D280D2E0D320D290D280D2C060B7C0B7B0B7B0B7D0B8C0B940000D38728DA0228E60063271063E26D 71 | [ 48518][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E26D 72 | [ 48527][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0001, Pack: 1 73 | [ 48536][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 74 | [ 48815][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014644E00201FD2E 75 | 76 | [ 48956][D][Pylon.cpp:260] ParseResponse(): received: 95 77 | [ 48960][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000110000000000000000000000000000000000600000000000000000000000600000000000031EECE 78 | [ 48967][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 79 | [ 48977][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 1 80 | [ 49322][D][Pack.cpp:12] PublishDiscovery(): Publishing discovery for Pack1 81 | [ 49850][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024642E00202FD2E 82 | 83 | [ 50043][D][Pylon.cpp:260] ParseResponse(): received: 141 84 | [ 50048][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0002100D310D340D320D340D330D320D2F0D320D320D320D300D320D2D0D320D320D30060B590B630B5C0B650B770B870000D31877CD0977FA001F6D6064E2EC 85 | [ 50059][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2EC 86 | [ 50068][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0002, Pack: 2 87 | [ 50077][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 1, Pack size: 5 88 | [ 50356][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024644E00202FD2C 89 | 90 | [ 50499][D][Pylon.cpp:260] ParseResponse(): received: 95 91 | [ 50502][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000210000000000000000000000000000000000600000000000000000000000600000000000000EED1 92 | [ 50509][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED1 93 | [ 50518][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 2 94 | [ 50863][D][Pack.cpp:12] PublishDiscovery(): Publishing discovery for Pack2 95 | [ 51393][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034642E00203FD2C 96 | 97 | [ 51591][D][Pylon.cpp:260] ParseResponse(): received: 141 98 | [ 51596][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0003100D2E0D380D360D330D350D3E0D3E0D360D370D330D220D2B0D460D320D2B0D31060B760B740B770B760B890B8D0000D34127C00227D60076271063E2EC 99 | [ 51607][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2EC 100 | [ 51616][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0003, Pack: 3 101 | [ 51625][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 2, Pack size: 5 102 | [ 51899][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034644E00203FD2A 103 | 104 | [ 52042][D][Pylon.cpp:260] ParseResponse(): received: 95 105 | [ 52047][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000310000000000000000000000000000000000600000000000000000000000600000000000000EED0 106 | [ 52054][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED0 107 | [ 52063][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 3 108 | [ 52405][D][Pack.cpp:12] PublishDiscovery(): Publishing discovery for Pack3 109 | [ 52934][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044642E00204FD2A 110 | 111 | [ 53128][D][Pylon.cpp:260] ParseResponse(): received: 141 112 | [ 53135][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0004100D2B0D360D400D350D3E0D3E0D360D310D280D3B0D270D2D0D330D330D470D36060B6F0B6C0B6C0B6C0B770B780000D35327CB0227E5007B271063E2A3 113 | [ 53146][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2A3 114 | [ 53156][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0004, Pack: 4 115 | [ 53165][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 3, Pack size: 5 116 | [ 53441][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044644E00204FD28 117 | 118 | [ 53582][D][Pylon.cpp:260] ParseResponse(): received: 95 119 | [ 53587][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000410000000000000000000000000000000000600000000000000000000000600000000000000EECF 120 | [ 53594][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECF 121 | [ 53604][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 4 122 | [ 53947][D][Pack.cpp:12] PublishDiscovery(): Publishing discovery for Pack4 123 | [ 54475][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054642E00205FD28 124 | 125 | [ 54669][D][Pylon.cpp:260] ParseResponse(): received: 141 126 | [ 54674][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0005100D2C0D2F0D2A0D3D0D3A0D3C0D2F0D310D320D290D2A0D2E0D410D3D0D390D2E060B5D0B5A0B5C0B600B710B710000D330282602283E007B271063E299 127 | [ 54685][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E299 128 | [ 54695][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0005, Pack: 5 129 | [ 54703][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 4, Pack size: 5 130 | [ 54982][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054644E00205FD26 131 | 132 | [ 55123][D][Pylon.cpp:260] ParseResponse(): received: 95 133 | [ 55128][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000510000000000000000000000000000000000600000000000000000000000600000000000000EECE 134 | [ 55135][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 135 | [ 55144][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 5 136 | [ 55489][D][Pack.cpp:12] PublishDiscovery(): Publishing discovery for Pack5 137 | 138 | [292599][D][Pylon.cpp:260] ParseResponse(): received: 141 139 | [292604][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0005100D2E0D2E0D2A0D380D340D370D2C0D300D320D290D2A0D2F0D3A0D380D340D2C060B5D0B5C0B5D0B630B690B6A0000D30B282C02283E007B271063E28C 140 | [292615][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E28C 141 | [292625][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0005, Pack: 5 142 | [292634][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 143 | [292911][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054644E00205FD26 144 | 145 | [293051][D][Pylon.cpp:260] ParseResponse(): received: 95 146 | [293054][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000510000000000000000000000000000000000600000000000000000000000600000000000000EECE 147 | [293061][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 148 | [293070][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 5 149 | [298424][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014642E00201FD30 150 | 151 | [298623][D][Pylon.cpp:260] ParseResponse(): received: 141 152 | [298628][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0001100D440D2F0D2F0D2F0D310D310D300D2F0D300D380D300D2E0D300D2F0D2F0D2D060B820B820B800B830B8B0B930000D36428830228E60063271063E2E2 153 | [298639][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E2 154 | [298648][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0001, Pack: 1 155 | [298657][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 156 | [298930][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014644E00201FD2E 157 | 158 | [299074][D][Pylon.cpp:260] ParseResponse(): received: 95 159 | [299079][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000110000000000000000000000000000000000600000000000000000000000600000000000031EECE 160 | [299085][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 161 | [299095][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 1 162 | [299945][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024642E00202FD2E 163 | 164 | [300144][D][Pylon.cpp:260] ParseResponse(): received: 141 165 | [300149][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0002100D2F0D320D300D320D300D300D2D0D300D310D310D2D0D310D2B0D2F0D310D2E060B5B0B680B5F0B690B780B860000D2F977D50977FA001F6D6064E297 166 | [300159][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E297 167 | [300169][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0002, Pack: 2 168 | [300178][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 169 | [300451][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024644E00202FD2C 170 | 171 | [300595][D][Pylon.cpp:260] ParseResponse(): received: 95 172 | [300600][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000210000000000000000000000000000000000600000000000000000000000600000000000000EED1 173 | [300607][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED1 174 | [300616][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 2 175 | [301465][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034642E00203FD2C 176 | 177 | [301658][D][Pylon.cpp:260] ParseResponse(): received: 141 178 | [301662][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0003100D2D0D340D350D300D330D390D380D340D330D300D270D2C0D3F0D300D2D0D30060B7C0B7A0B7B0B7D0B880B8B0000D32027140227D60075271062E2E8 179 | [301673][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E8 180 | [301683][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0003, Pack: 3 181 | [301691][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 182 | [301970][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034644E00203FD2A 183 | 184 | [302116][D][Pylon.cpp:260] ParseResponse(): received: 95 185 | [302119][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000310000000000000000000000000000000000600000000000000000000000600000000000000EED0 186 | [302126][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED0 187 | [302136][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 3 188 | [302984][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044642E00204FD2A 189 | 190 | [303183][D][Pylon.cpp:260] ParseResponse(): received: 141 191 | [303188][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0004100D280D310D3C0D350D3C0D3B0D360D310D260D370D240D2A0D2F0D320D460D33060B730B720B6F0B720B740B750000D32D27D10227E5007B271063E2E2 192 | [303199][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E2 193 | [303208][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0004, Pack: 4 194 | [303217][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 195 | [303491][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044644E00204FD28 196 | 197 | [303631][D][Pylon.cpp:260] ParseResponse(): received: 95 198 | [303636][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000410000000000000000000000000000000000600000000000000000000000600000000000000EECF 199 | [303643][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECF 200 | [303652][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 4 201 | [304504][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054642E00205FD28 202 | 203 | [304697][D][Pylon.cpp:260] ParseResponse(): received: 141 204 | [304701][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0005100D2E0D2E0D2B0D380D340D370D2D0D300D320D290D2A0D300D3A0D380D340D2C060B5D0B5C0B5D0B630B690B6A0000D30E282C02283E007B271063E29C 205 | [304712][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E29C 206 | [304722][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0005, Pack: 5 207 | [304730][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 208 | [305009][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054644E00205FD26 209 | 210 | [305151][D][Pylon.cpp:260] ParseResponse(): received: 95 211 | [305155][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000510000000000000000000000000000000000600000000000000000000000600000000000000EECE 212 | [305161][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 213 | [305171][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 5 214 | [310522][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014642E00201FD30 215 | 216 | [310716][D][Pylon.cpp:260] ParseResponse(): received: 141 217 | [310721][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0001100D440D2F0D2F0D2F0D310D310D300D2F0D300D380D300D2E0D300D2F0D2F0D2D060B820B820B800B830B8B0B930000D36428830228E60063271063E2E2 218 | [310732][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E2 219 | [310741][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0001, Pack: 1 220 | [310750][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 221 | [311028][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014644E00201FD2E 222 | 223 | [311168][D][Pylon.cpp:260] ParseResponse(): received: 95 224 | [311172][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000110000000000000000000000000000000000600000000000000000000000600000000000031EECE 225 | [311179][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 226 | [311189][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 1 227 | [312041][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024642E00202FD2E 228 | 229 | [312237][D][Pylon.cpp:260] ParseResponse(): received: 141 230 | [312242][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0002100D2F0D300D2F0D310D310D300D2D0D300D310D300D2D0D300D2B0D2F0D300D2D060B5B0B680B5F0B690B780B860000D2F277D50977FA001F6D6064E28F 231 | [312253][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E28F 232 | [312262][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0002, Pack: 2 233 | [312271][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 234 | [312547][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25024644E00202FD2C 235 | 236 | [312688][D][Pylon.cpp:260] ParseResponse(): received: 95 237 | [312692][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000210000000000000000000000000000000000600000000000000000000000600000000000000EED1 238 | [312699][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED1 239 | [312708][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 2 240 | [313561][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034642E00203FD2C 241 | 242 | [313755][D][Pylon.cpp:260] ParseResponse(): received: 141 243 | [313759][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0003100D2D0D350D350D300D330D390D380D340D330D300D270D2B0D3F0D300D2D0D30060B7C0B7A0B7B0B7D0B880B8B0000D32027140227D60075271062E2E8 244 | [313770][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E8 245 | [313780][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0003, Pack: 3 246 | [313789][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 247 | [314067][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25034644E00203FD2A 248 | 249 | [314216][D][Pylon.cpp:260] ParseResponse(): received: 95 250 | [314219][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000310000000000000000000000000000000000600000000000000000000000600000000000000EED0 251 | [314226][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EED0 252 | [314236][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 3 253 | [315081][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044642E00204FD2A 254 | 255 | [315276][D][Pylon.cpp:260] ParseResponse(): received: 141 256 | [315281][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0004100D280D310D3C0D350D3C0D3B0D360D310D260D370D240D2A0D2F0D320D460D32060B730B720B6F0B720B740B750000D32C27D10227E5007B271063E2E4 257 | [315291][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2E4 258 | [315301][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0004, Pack: 4 259 | [315310][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 260 | [315588][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25044644E00204FD28 261 | 262 | [315728][D][Pylon.cpp:260] ParseResponse(): received: 95 263 | [315733][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000410000000000000000000000000000000000600000000000000000000000600000000000000EECF 264 | [315740][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECF 265 | [315749][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 4 266 | [316601][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054642E00205FD28 267 | 268 | [316802][D][Pylon.cpp:260] ParseResponse(): received: 141 269 | [316806][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0005100D2E0D2E0D2B0D380D340D370D2D0D300D320D290D2A0D2F0D3B0D380D340D2C060B5D0B5C0B5D0B630B690B6A0000D30E282C02283E007B271063E286 270 | [316817][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E286 271 | [316826][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0005, Pack: 5 272 | [316835][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 273 | [317106][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25054644E00205FD26 274 | 275 | [317247][D][Pylon.cpp:260] ParseResponse(): received: 95 276 | [317252][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000510000000000000000000000000000000000600000000000000000000000600000000000000EECE 277 | [317259][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 278 | [317268][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 5 279 | [322619][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014642E00201FD30 280 | 281 | [322813][D][Pylon.cpp:260] ParseResponse(): received: 141 282 | [322817][D][Pylon.cpp:261] ParseResponse(): data: ~25014600D07C0001100D440D2F0D2F0D2F0D300D300D2F0D2E0D300D380D300D2E0D300D2F0D2F0D2D060B820B820B800B830B8B0B930000D36028830228E60063271063E2D4 283 | [322828][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 7C (124), CHKSUM: E2D4 284 | [322837][I][Pylon.cpp:307] ParseResponse(): AnalogValueFixedPoint: INFO: 0001, Pack: 1 285 | [322846][D][Pylon.cpp:331] ParseResponse(): AnalogValueFixedPoint: packIndex: 0, Pack size: 5 286 | [323125][D][Pylon.cpp:218] send_cmd(): send_cmd: ~25014644E00201FD2E 287 | 288 | [323266][D][Pylon.cpp:260] ParseResponse(): received: 95 289 | [323271][D][Pylon.cpp:261] ParseResponse(): data: ~25014600E04E000110000000000000000000000000000000000600000000000000000000000600000000000031EECE 290 | [323278][D][Pylon.cpp:295] ParseResponse(): VER: 25, ADR: 01, CID1: 46, CID2: 00, LENID: 4E (78), CHKSUM: EECE 291 | [323287][I][Pylon.cpp:374] ParseResponse(): GetAlarm: Pack: 1 -------------------------------------------------------------------------------- /Docs/docker hub notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 1. If not in .ssh, go to docker hub, login and get access token in account settings->Security New Access Token. ( 3 | 2. On the system that has the pylon_to_mqtt running: docker login 4 | 3. make sure PylonToMQTT container image is ready to upload to docker-hub with tag classicdiy/pylon_to_mqtt:latest 5 | 4. docker push classicdiy/pylon_to_mqtt:latest 6 | 7 | env 8 | export COMPOSE_HTTP_TIMEOUT=120 9 | export LOGLEVEL=INFO 10 | 11 | Influx 12 | use mqtt_pylon 13 | delete from mqtt_consumer 14 | 15 | clear logs 16 | 17 | docker ps 18 | sudo sh -c 'echo "" > $(docker inspect --format="{{.LogPath}}" pylon_to_mqtt)' 19 | sudo sh -c 'echo "" > $(docker inspect --format="{{.LogPath}}" grafana)' 20 | sudo sh -c 'echo "" > $(docker inspect --format="{{.LogPath}}" influxdb)' 21 | sudo sh -c 'echo "" > $(docker inspect --format="{{.LogPath}}" telegraf)' 22 | sudo sh -c 'echo "" > $(docker inspect --format="{{.LogPath}}" portainer-ce)' 23 | docker logs pylon_to_mqtt --tail 100 24 | docker logs grafana --tail 100 25 | docker logs influxdb --tail 100 26 | docker logs telegraf --tail 100 27 | docker logs portainer-ce --tail 100 28 | 29 | 30 | docker run -d --name pylon_to_mqtt --device=/dev/ttyUSB0:/dev/ttyUSB0 pylon_to_mqtt --pylon_port /dev/ttyUSB0 --mqtt_host 192.168.86.23 -------------------------------------------------------------------------------- /New dashboard-1667136343536.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_INFLUXDB", 5 | "label": "InfluxDB", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "influxdb", 9 | "pluginName": "InfluxDB" 10 | } 11 | ], 12 | "__elements": {}, 13 | "__requires": [ 14 | { 15 | "type": "grafana", 16 | "id": "grafana", 17 | "name": "Grafana", 18 | "version": "9.2.2" 19 | }, 20 | { 21 | "type": "datasource", 22 | "id": "influxdb", 23 | "name": "InfluxDB", 24 | "version": "1.0.0" 25 | }, 26 | { 27 | "type": "panel", 28 | "id": "timeseries", 29 | "name": "Time series", 30 | "version": "" 31 | } 32 | ], 33 | "annotations": { 34 | "list": [ 35 | { 36 | "builtIn": 1, 37 | "datasource": { 38 | "type": "grafana", 39 | "uid": "-- Grafana --" 40 | }, 41 | "enable": true, 42 | "hide": true, 43 | "iconColor": "rgba(0, 211, 255, 1)", 44 | "name": "Annotations & Alerts", 45 | "target": { 46 | "limit": 100, 47 | "matchAny": false, 48 | "tags": [], 49 | "type": "dashboard" 50 | }, 51 | "type": "dashboard" 52 | } 53 | ] 54 | }, 55 | "editable": true, 56 | "fiscalYearStartMonth": 0, 57 | "graphTooltip": 0, 58 | "id": null, 59 | "links": [], 60 | "liveNow": false, 61 | "panels": [ 62 | { 63 | "datasource": { 64 | "type": "influxdb", 65 | "uid": "${DS_INFLUXDB}" 66 | }, 67 | "fieldConfig": { 68 | "defaults": { 69 | "color": { 70 | "mode": "palette-classic" 71 | }, 72 | "custom": { 73 | "axisCenteredZero": false, 74 | "axisColorMode": "text", 75 | "axisLabel": "", 76 | "axisPlacement": "auto", 77 | "barAlignment": 0, 78 | "drawStyle": "line", 79 | "fillOpacity": 0, 80 | "gradientMode": "none", 81 | "hideFrom": { 82 | "legend": false, 83 | "tooltip": false, 84 | "viz": false 85 | }, 86 | "lineInterpolation": "linear", 87 | "lineWidth": 1, 88 | "pointSize": 5, 89 | "scaleDistribution": { 90 | "type": "linear" 91 | }, 92 | "showPoints": "auto", 93 | "spanNulls": false, 94 | "stacking": { 95 | "group": "A", 96 | "mode": "none" 97 | }, 98 | "thresholdsStyle": { 99 | "mode": "off" 100 | } 101 | }, 102 | "mappings": [], 103 | "thresholds": { 104 | "mode": "absolute", 105 | "steps": [ 106 | { 107 | "color": "green", 108 | "value": null 109 | }, 110 | { 111 | "color": "red", 112 | "value": 80 113 | } 114 | ] 115 | } 116 | }, 117 | "overrides": [] 118 | }, 119 | "gridPos": { 120 | "h": 9, 121 | "w": 12, 122 | "x": 0, 123 | "y": 0 124 | }, 125 | "id": 2, 126 | "options": { 127 | "legend": { 128 | "calcs": [], 129 | "displayMode": "list", 130 | "placement": "bottom", 131 | "showLegend": true 132 | }, 133 | "tooltip": { 134 | "mode": "single", 135 | "sort": "none" 136 | } 137 | }, 138 | "targets": [ 139 | { 140 | "datasource": { 141 | "type": "influxdb", 142 | "uid": "${DS_INFLUXDB}" 143 | }, 144 | "groupBy": [ 145 | { 146 | "params": [ 147 | "$__interval" 148 | ], 149 | "type": "time" 150 | }, 151 | { 152 | "params": [ 153 | "null" 154 | ], 155 | "type": "fill" 156 | } 157 | ], 158 | "measurement": "mqtt_consumer", 159 | "orderByTime": "ASC", 160 | "policy": "default", 161 | "refId": "A", 162 | "resultFormat": "time_series", 163 | "select": [ 164 | [ 165 | { 166 | "params": [ 167 | "Cells_Cell_10_Reading" 168 | ], 169 | "type": "field" 170 | }, 171 | { 172 | "params": [], 173 | "type": "mean" 174 | } 175 | ] 176 | ], 177 | "tags": [ 178 | { 179 | "key": "topic", 180 | "operator": "=", 181 | "value": "PylonToMQTT/Main/stat/readings/Pack1" 182 | } 183 | ] 184 | } 185 | ], 186 | "title": "Panel Title", 187 | "type": "timeseries" 188 | } 189 | ], 190 | "schemaVersion": 37, 191 | "style": "dark", 192 | "tags": [], 193 | "templating": { 194 | "list": [] 195 | }, 196 | "time": { 197 | "from": "now-6h", 198 | "to": "now" 199 | }, 200 | "timepicker": {}, 201 | "timezone": "", 202 | "title": "New dashboard", 203 | "uid": "cpFDvsRgz", 204 | "version": 1, 205 | "weekStart": "" 206 | } -------------------------------------------------------------------------------- /Pictures/BMSStats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/BMSStats.png -------------------------------------------------------------------------------- /Pictures/DCE RS232 Adapter.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/DCE RS232 Adapter.PNG -------------------------------------------------------------------------------- /Pictures/Flasher1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Flasher1.PNG -------------------------------------------------------------------------------- /Pictures/Flasher3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Flasher3.PNG -------------------------------------------------------------------------------- /Pictures/Flasher5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Flasher5.PNG -------------------------------------------------------------------------------- /Pictures/Flasher6.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Flasher6.PNG -------------------------------------------------------------------------------- /Pictures/Flasher7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Flasher7.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_BuildStack.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_BuildStack.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_Containers.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_Containers.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_MQTTLogs.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_MQTTLogs.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_MQTTfxSetup.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_MQTTfxSetup.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_MQTTfxSubscribe.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_MQTTfxSubscribe.PNG -------------------------------------------------------------------------------- /Pictures/IOTStack_docker_ps.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/IOTStack_docker_ps.PNG -------------------------------------------------------------------------------- /Pictures/Jakiper RJ11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Jakiper RJ11.jpg -------------------------------------------------------------------------------- /Pictures/Jakiper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/Jakiper.png -------------------------------------------------------------------------------- /Pictures/PylonToMQTT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/PylonToMQTT.png -------------------------------------------------------------------------------- /Pictures/RS-232_DE-9_Connector_Pinouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/RS-232_DE-9_Connector_Pinouts.png -------------------------------------------------------------------------------- /Pictures/RS232_Wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/RS232_Wiring.png -------------------------------------------------------------------------------- /Pictures/WIP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/WIP.jpg -------------------------------------------------------------------------------- /Pictures/grafana.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClassicDIY/PylonToMQTT/ac2eab219584a965bc0c72345d04be0ea8d75a45/Pictures/grafana.PNG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PylonToMQTT 2 | 3 |

    Jakiper server rack Lithium battery to MQTT publisher

    4 | 5 | Buy Me A Coffee 6 | 7 | [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FClassicDIY%2FJakiperMonitor&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) 8 | 9 | 14 | 15 | 16 | 17 |

    18 | The PylonToMQTT publisher will read data from your Jakiper Battery Bank using the Pylontech protocol via the RS232 console port and publish the data to a MQTT broker. This allows you to integrate other MQTT subscriber applications like HomeAssistant, NodeRed, InfluxDB and Grafana. 19 | 20 | The software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied. 21 |

    22 | 23 |

    24 | Please refer to the PylonToMQTT wiki for more information. 25 |

    26 | 27 |

    28 | Support for the US2000 Pylontech batteries can be found at tomascrespo/pylon2mqtt. 29 |

    30 | 31 | ## License 32 | ``` 33 | 34 | Copyright (c) 2022 35 | 36 | Licensed under the Apache License, Version 2.0 (the "License"); 37 | you may not use this file except in compliance with the License. 38 | You may obtain a copy of the License at 39 | 40 | http://www.apache.org/licenses/LICENSE-2.0 41 | 42 | Unless required by applicable law or agreed to in writing, software 43 | distributed under the License is distributed on an "AS IS" BASIS, 44 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 45 | See the License for the specific language governing permissions and 46 | limitations under the License. 47 | 48 | ``` 49 | --------------------------------------------------------------------------------