diff --git a/HMI/nspanel.HMI b/HMI/nspanel.HMI new file mode 100644 index 00000000..f029b88f Binary files /dev/null and b/HMI/nspanel.HMI differ diff --git a/HMI/nspanel.tft b/HMI/nspanel.tft new file mode 100644 index 00000000..247c64b1 Binary files /dev/null and b/HMI/nspanel.tft differ diff --git a/components/nextion_custom/__init__.py b/components/nextion_custom/__init__.py new file mode 100644 index 00000000..beeeccc4 --- /dev/null +++ b/components/nextion_custom/__init__.py @@ -0,0 +1,62 @@ +from esphome import automation +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import uart, time, switch, sensor +from esphome.const import ( + CONF_ID, + CONF_NAME, + CONF_ON_CLICK, + CONF_TEMPERATURE, + CONF_THEN, + CONF_TIME_ID, + CONF_TYPE, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["text_sensor"] +CODEOWNERS = ["@joBr99"] +DEPENDENCIES = ["uart", "wifi", "esp32"] + + +nextion_custom_ns = cg.esphome_ns.namespace("NextionCustom") +NextionCustom = nextion_custom_ns.class_("NextionCustom", cg.Component, uart.UARTDevice) + + +NextionCustomMsgIncomingTrigger = nextion_custom_ns.class_( + "NextionCustomMsgIncomingTrigger", + automation.Trigger.template(cg.std_string) +) + +CONF_INCOMING_MSG = 'on_incoming_msg' + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(NextionCustom), + cv.Required(CONF_INCOMING_MSG): automation.validate_automation( + cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextionCustomMsgIncomingTrigger), + } + ) + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + for conf in config.get(CONF_INCOMING_MSG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + cg.add_define("USE_NEXTIONCUSTOM") \ No newline at end of file diff --git a/components/nextion_custom/automation.h b/components/nextion_custom/automation.h new file mode 100644 index 00000000..ec0a7d0c --- /dev/null +++ b/components/nextion_custom/automation.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "nextion_custom.h" + +namespace esphome { +namespace NextionCustom { + +class NextionCustomMsgIncomingTrigger : public Trigger { + public: + explicit NextionCustomMsgIncomingTrigger(NextionCustom *parent) { + parent->add_incoming_msg_callback([this](const std::string &value) { this->trigger(value); }); + } +}; + +} // namespace NextionCustom +} // namespace esphome \ No newline at end of file diff --git a/components/nextion_custom/nextion_custom.cpp b/components/nextion_custom/nextion_custom.cpp new file mode 100644 index 00000000..a3b86258 --- /dev/null +++ b/components/nextion_custom/nextion_custom.cpp @@ -0,0 +1,171 @@ +#include "nextion_custom.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "esphome/components/wifi/wifi_component.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/util.h" +#include "esphome/core/version.h" + +namespace esphome { +namespace NextionCustom { + +static const char *const TAG = "nextion_custom"; + +static const char *const SUCCESS_RESPONSE = "{\"error\":0}"; + +static const uint8_t WAKE_RESPONSE[7] = {0xFF, 0xFF, 0xFF, 0x88, 0xFF, 0xFF, 0xFF}; + +void NextionCustom::setup() { +} + +void NextionCustom::loop() { + uint8_t d; + + while (this->available()) { + this->read_byte(&d); + this->buffer_.push_back(d); + //ESP_LOGD(TAG, "ReceivedD: 0x%02x", d); + if (!this->process_data_()) { + ESP_LOGD(TAG, "Received: 0x%02x", d); + this->buffer_.clear(); + } + } + +} + +bool NextionCustom::process_data_() { + uint32_t at = this->buffer_.size() - 1; + auto *data = &this->buffer_[0]; + uint8_t new_byte = data[at]; + +// if (data[0] == WAKE_RESPONSE[0]) { // Screen wake message +// if (at < 6) +// return new_byte == WAKE_RESPONSE[at]; +// if (new_byte == WAKE_RESPONSE[at]) { +// ESP_LOGD(TAG, "Screen wake message received"); +// this->initialize(); +// return false; +// } +// return false; +// } +// + // Byte 0: HEADER1 (always 0x55) + if (at == 0) + return new_byte == 0x55; + // Byte 1: HEADER2 (always 0xBB) + if (at == 1) + return new_byte == 0xBB; + + // length + if (at == 2){ + return true; + } + uint8_t length = data[2]; + + // Wait until all data comes in + if (at - 3 < length){ + //ESP_LOGD(TAG, "Message Length: (%d/%d)", at - 2, length); + //ESP_LOGD(TAG, "Received: 0x%02x", new_byte); + return true; + } + + + // Wait for CRC + if (at == 2 + length || at == 2 + length + 1) + return true; + + uint16_t crc16 = encode_uint16(data[3 + length + 1], data[3 + length]); + uint16_t calculated_crc16 = NextionCustom::crc16(data, 3 + length); + + if (crc16 != calculated_crc16) { + ESP_LOGW(TAG, "Received invalid message checksum %02X!=%02X", crc16, calculated_crc16); + return false; + }else{ + ESP_LOGD(TAG, "Received valid message checksum %02X==%02X", crc16, calculated_crc16); + } + + const uint8_t *message_data = data + 3; + std::string message(message_data, message_data + length); + + #if ESPHOME_VERSION_CODE >= VERSION_CODE(2022, 1, 0) + ESP_LOGD(TAG, "Received NextionCustom: PAYLOAD=%s RAW=[%s]", message.c_str(), + format_hex_pretty(message_data, length).c_str()); + #else + ESP_LOGD(TAG, "Received NextionCustom: PAYLOAD=%s RAW=[%s]", message.c_str(), + hexencode(message_data, length).c_str()); + #endif + this->process_command_(message); + return false; +} + +void NextionCustom::process_command_(const std::string &message) { + this->incoming_msg_callback_.call(message); +} + +void NextionCustom::add_incoming_msg_callback(std::function callback) { + this->incoming_msg_callback_.add(std::move(callback)); +} + +void NextionCustom::dump_config() { ESP_LOGCONFIG(TAG, "NextionCustom:"); } + + +void NextionCustom::send_command_(const std::string &command) { + ESP_LOGD(TAG, "Sending: %s", command.c_str()); + this->write_str(command.c_str()); + const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; + this->write_array(to_send, sizeof(to_send)); +} + +void NextionCustom::send_special_hex_cmd(const std::string &command) { + ESP_LOGD(TAG, "Sending: special hex cmd"); + + const uint8_t to_send[3] = {0x09, 0x19, 0x08}; + this->write_array(to_send, sizeof(to_send)); + //this->write_str(" fffb 0002 0000 0020"); + this->write_str(command.c_str()); + const uint8_t to_send2[3] = {0xFF, 0xFF, 0xFF}; + this->write_array(to_send2, sizeof(to_send2)); +} + +void NextionCustom::send_custom_command(const std::string &command) { + ESP_LOGD(TAG, "Sending custom command: %s", command.c_str()); + std::vector data = {0x55, 0xBB}; + data.push_back(command.length() & 0xFF); + //data.push_back((command.length() >> 8) & 0xFF); + data.insert(data.end(), command.begin(), command.end()); + auto crc = crc16(data.data(), data.size()); + data.push_back(crc & 0xFF); + data.push_back((crc >> 8) & 0xFF); + this->write_array(data); + + #if ESPHOME_VERSION_CODE >= VERSION_CODE(2022, 1, 0) + ESP_LOGD(TAG, "Send NextionCustom: PAYLOAD=%s RAW=[%s]", command.c_str(), + format_hex_pretty(&data[0], data.size()).c_str()); + #else + ESP_LOGD(TAG, "Send NextionCustom: PAYLOAD=%s RAW=[%s]", command.c_str(), + hexencode(&data[0], data.size()).c_str()); + #endif +} + +uint16_t NextionCustom::crc16(const uint8_t *data, uint16_t len) { + uint16_t crc = 0xFFFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +} // namespace NextionCustom +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO \ No newline at end of file diff --git a/components/nextion_custom/nextion_custom.h b/components/nextion_custom/nextion_custom.h new file mode 100644 index 00000000..5edbe2a4 --- /dev/null +++ b/components/nextion_custom/nextion_custom.h @@ -0,0 +1,52 @@ +#pragma once + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include + +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/switch/switch.h" +//#include "esphome/components/time/real_time_clock.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace NextionCustom { + +class NextionCustom : public Component, public uart::UARTDevice { + public: + void setup() override; + void loop() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void send_custom_command(const std::string &command); + + + void send_special_hex_cmd(const std::string &command); + + void add_incoming_msg_callback(std::function callback); + + void send_command_(const std::string &command); + + + protected: + static uint16_t crc16(const uint8_t *data, uint16_t len); + + bool process_data_(); + void process_command_(const std::string &message); + + CallbackManager incoming_msg_callback_; + + std::vector buffer_; +}; + + +} // namespace NextionCustom +} // namespace esphome + +#endif // USE_ESP32_FRAMEWORK_ARDUINO \ No newline at end of file diff --git a/esphome.yaml b/esphome.yaml new file mode 100644 index 00000000..f11dc0d9 --- /dev/null +++ b/esphome.yaml @@ -0,0 +1,208 @@ +# Set some variables for convenience +substitutions: + node_name: nspanel-test + device_name: Test NSPanel + +external_components: + - source: github://joBr99/nspanel-lovelance-ui@main + components: [nextion_custom] + refresh: 1h + +web_server: + port: 80 + +esphome: + name: $node_name + comment: $device_name + +esp32: + board: esp32dev + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +# Enable logging +logger: + +# Enable wireless updates +ota: + +# A reboot button is always useful +button: + - platform: restart + name: Restart $device_name + +# Define some inputs +binary_sensor: + - platform: gpio + name: $device_name Left Button + pin: + number: 14 + inverted: true + on_click: + - switch.toggle: relay_1 + + - platform: gpio + name: $device_name Right Button + pin: + number: 27 + inverted: true + on_click: + - switch.toggle: relay_2 + +sensor: + - platform: wifi_signal + name: $device_name WiFi Signal + update_interval: 60s + + - platform: ntc + id: temperature + sensor: resistance_sensor + calibration: + b_constant: 3950 + reference_temperature: 25°C + reference_resistance: 10kOhm + name: $device_name Temperature + + - platform: resistance + id: resistance_sensor + sensor: ntc_source + configuration: DOWNSTREAM + resistor: 11.2kOhm + + - platform: adc + id: ntc_source + pin: 38 + update_interval: 10s + attenuation: 11db + + +# Define some outputs +switch: + # The two relays + - platform: gpio + name: $device_name Relay 1 + id: relay_1 + pin: + number: 22 + - platform: gpio + name: $device_name Relay 2 + id: relay_2 + pin: + number: 19 + + # Pin 4 always needs to be on to power up the display + - platform: gpio + id: screen_power + entity_category: config + pin: + number: 4 + inverted: true + restore_mode: ALWAYS_ON + +#number: +# platform: template +# name: $device_name Brightness +# id: brightness +# entity_category: config +# unit_of_measurement: '%' +# min_value: 0 +# max_value: 100 +# step: 1 +# initial_value: 30 +# set_action: +# then: +# - lambda: 'id(disp1).set_backlight_brightness(x/100);' + +# Configure the internal bleeper +output: + - platform: ledc + id: buzzer_out + pin: + number: 21 + +# Enable ringtone music support +rtttl: + id: buzzer + output: buzzer_out + +# Configure UART for communicating with the screen +uart: + id: tf_uart + tx_pin: 16 + rx_pin: 17 + baud_rate: 115200 + + +# Configure the screen itself +#display: +# - platform: nextion +# id: disp1 +# uart_id: tf_uart +# tft_url: http://192.168.75.30:8123/local/nspanel-custom.tft +#api: +# services: +# - service: upload_tft +# then: +# - lambda: 'id(disp1)->upload_tft();' + +nextion_custom: + id: nextcustom_id + on_incoming_msg: + then: + - lambda: |- + ESP_LOGD("KEKW", "Got incoming message from nextion panel %s", x.c_str()); + + std::string input = x; + std::string word = ""; + std::vector args; + + while (input.compare(word) != 0) + { + auto index = input.find_first_of(","); + word = input.substr(0,index); + input = input.substr(index+1, input.length()); + args.push_back(word); + } + + int pages = 2; + if(args.at(0) == "event"){ + if(args.at(1) == "pageOpen"){ + int recvPage = std::atoi(args.at(2).c_str()); + int destPage = abs(recvPage%pages); + ESP_LOGD("KEKW", "navigate to page: %d", destPage); + + if(destPage == 0 ){ + id(nextcustom_id).send_custom_command("entityUpdHeading,Rolladen"); + id(nextcustom_id).send_custom_command("entityUpd,1,0,Fenster Eingang,shutter"); + id(nextcustom_id).send_custom_command("entityUpd,2,0,Fenster Terrasse,shutter"); + id(nextcustom_id).send_custom_command("entityUpd,3,0,Terrassentür,shutter"); + } + if(destPage == 1 ){ + id(nextcustom_id).send_custom_command("entityUpdHeading,Page2"); + id(nextcustom_id).send_custom_command("entityUpd,1,1,test,light,0"); + id(nextcustom_id).send_custom_command("entityUpd,2,0,test,shutter"); + id(nextcustom_id).send_custom_command("entityUpd,3,1,test,light,1"); + id(nextcustom_id).send_custom_command("entityUpd,4,1,test,light,1"); + } + } + } + + + +# Enable Home Assistant API +api: + services: + - service: send_nextion_custom + variables: + cmd: string + then: + - lambda: |- + id(nextcustom_id).send_custom_command(cmd); + - service: send_nextion_cmd + variables: + cmd: string + then: + - lambda: |- + id(nextcustom_id).send_command_(cmd); \ No newline at end of file