├── pictures ├── MaxSpeed.png ├── ManualMaxMain.png ├── RS485_Adapter.jpg ├── ManualChargeSpeed.png ├── HomeAssistant_ChargeMode.png ├── ESP_-_32_NodeMCU_Developmentboard_Pinout_Diagram.jpg └── esp32-nodemcu-module-wlan-wifi-development-board-mit-cp2102-nachfolgermodell-zum-esp8266-kompatibel-mit-arduino-872375_400x.webp ├── input_selects.yaml ├── input_numbers.yaml ├── LICENSE.md ├── evcc_charge_automation.yaml ├── .gitignore ├── esphome └── components │ └── modbus_server │ ├── modbus_server.cpp │ ├── modbus_server.h │ └── __init__.py ├── README.md ├── sample_output └── serial_output.log ├── charge_automation.yaml └── esphome-xemex-fake-modbus-server.yaml /pictures/MaxSpeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/MaxSpeed.png -------------------------------------------------------------------------------- /pictures/ManualMaxMain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/ManualMaxMain.png -------------------------------------------------------------------------------- /pictures/RS485_Adapter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/RS485_Adapter.jpg -------------------------------------------------------------------------------- /pictures/ManualChargeSpeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/ManualChargeSpeed.png -------------------------------------------------------------------------------- /pictures/HomeAssistant_ChargeMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/HomeAssistant_ChargeMode.png -------------------------------------------------------------------------------- /pictures/ESP_-_32_NodeMCU_Developmentboard_Pinout_Diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/ESP_-_32_NodeMCU_Developmentboard_Pinout_Diagram.jpg -------------------------------------------------------------------------------- /pictures/esp32-nodemcu-module-wlan-wifi-development-board-mit-cp2102-nachfolgermodell-zum-esp8266-kompatibel-mit-arduino-872375_400x.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomase1234/esphome-fake-xemex-csmb/HEAD/pictures/esp32-nodemcu-module-wlan-wifi-development-board-mit-cp2102-nachfolgermodell-zum-esp8266-kompatibel-mit-arduino-872375_400x.webp -------------------------------------------------------------------------------- /input_selects.yaml: -------------------------------------------------------------------------------- 1 | input_select: 2 | charge_mode: 3 | name: Charge Mode 4 | options: 5 | - Max Speed 6 | - Avoid Power To Grid 7 | - 2A From Grid 8 | - Solar only 9 | - Manual Charge Speed 10 | - Manual Max Main 11 | - Charging Off 12 | initial: Max Speed 13 | icon: mdi:ev-station 14 | -------------------------------------------------------------------------------- /input_numbers.yaml: -------------------------------------------------------------------------------- 1 | chargepoint_desired_current: 2 | name: Chargepoint Desired Current 3 | min: 0 4 | max: 60 5 | step: 0.001 6 | unit_of_measurement: "A" 7 | icon: mdi:ev-station 8 | 9 | chargepoint_minimal_current: 10 | name: Minimal Current to start charging 11 | min: 1 12 | max: 16 13 | step: 1 14 | initial: 9 15 | unit_of_measurement: "A" 16 | icon: mdi:ev-station 17 | 18 | chargepoint_manual_current: 19 | name: Chargepoint Manual Current 20 | min: 8 21 | max: 60 22 | step: 1 23 | initial: 16 24 | mode: box 25 | unit_of_measurement: "A" 26 | icon: mdi:ev-station 27 | 28 | main_maximal_current: 29 | name: Main Fuse Max Current 30 | min: 10 31 | max: 40 32 | step: 0.1 33 | initial: 37.8 34 | mode: box 35 | unit_of_measurement: "A" 36 | icon: mdi:home 37 | 38 | main_manual_current: 39 | name: Main Fuse Manual Current 40 | min: 10 41 | max: 40 42 | step: 1 43 | initial: 25 44 | mode: box 45 | unit_of_measurement: "A" 46 | icon: mdi:home -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Epic Labs 4 | Copyright (c) 2024 Thomas Elsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /evcc_charge_automation.yaml: -------------------------------------------------------------------------------- 1 | alias: EVCC Chargepoint Current Regulation 2 | description: "" 3 | trigger: 4 | - platform: time_pattern 5 | seconds: /1 6 | enabled: true 7 | action: 8 | - choose: # start of check if increasing or decreasing 9 | - conditions: 10 | - condition: state 11 | entity_id: input_boolean.evcc_enable_charger 12 | state: "off" 13 | sequence: 14 | - action: number.set_value 15 | metadata: {} 16 | data: 17 | value: "50" 18 | target: 19 | entity_id: number.modbus_server_xemex_ct1current 20 | - conditions: 21 | - condition: template 22 | value_template: >- ## if load should be increased 23 | {{(states('input_number.evcc_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) ) > 0 }} 24 | sequence: 25 | - service: number.set_value 26 | data: 27 | value: >- 28 | {{ states('input_number.main_maximal_current') | float() - (states('input_number.evcc_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) )}} 29 | target: 30 | entity_id: number.modbus_server_xemex_ct1current 31 | - conditions: 32 | - condition: template 33 | value_template: >- ## if load should be decreased 34 | {{(states('input_number.evcc_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) ) <= 0 }} 35 | sequence: 36 | - service: number.set_value 37 | data: 38 | value: >- 39 | {{ states('input_number.main_maximal_current') | float() - (states('input_number.evcc_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) )/2}} 40 | target: 41 | entity_id: number.modbus_server_xemex_ct1current 42 | enabled: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Hide sublime text stuff 10 | *.sublime-project 11 | *.sublime-workspace 12 | 13 | # Intellij Idea 14 | .idea 15 | 16 | # Vim 17 | *.swp 18 | 19 | # Hide some OS X stuff 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Distribution / packaging 29 | .Python 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | .esphome 58 | nosetests.xml 59 | coverage.xml 60 | cov.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # pyenv 70 | .python-version 71 | 72 | # Environments 73 | .env 74 | .venv 75 | env/ 76 | venv/ 77 | ENV/ 78 | env.bak/ 79 | venv.bak/ 80 | venv-*/ 81 | 82 | # mypy 83 | .mypy_cache/ 84 | 85 | .pioenvs 86 | .piolibdeps 87 | .pio 88 | .vscode/ 89 | !.vscode/tasks.json 90 | CMakeListsPrivate.txt 91 | CMakeLists.txt 92 | 93 | # User-specific stuff: 94 | .idea/**/workspace.xml 95 | .idea/**/tasks.xml 96 | .idea/dictionaries 97 | 98 | # Sensitive or high-churn files: 99 | .idea/**/dataSources/ 100 | .idea/**/dataSources.ids 101 | .idea/**/dataSources.xml 102 | .idea/**/dataSources.local.xml 103 | .idea/**/dynamic.xml 104 | 105 | # CMake 106 | cmake-build-*/ 107 | 108 | CMakeCache.txt 109 | CMakeFiles 110 | CMakeScripts 111 | Testing 112 | Makefile 113 | cmake_install.cmake 114 | install_manifest.txt 115 | compile_commands.json 116 | CTestTestfile.cmake 117 | /*.cbp 118 | 119 | .clang_complete 120 | .gcc-flags.json 121 | 122 | config/ 123 | tests/build/ 124 | tests/.esphome/ 125 | /.temp-clang-tidy.cpp 126 | /.temp/ 127 | .pio/ 128 | 129 | sdkconfig.* 130 | !sdkconfig.defaults 131 | esphome/components/homelua/ 132 | -------------------------------------------------------------------------------- /esphome/components/modbus_server/modbus_server.cpp: -------------------------------------------------------------------------------- 1 | // This ESPHome component wraps around the modbus-esp8266 by @emelianov: 2 | // https://github.com/emelianov/modbus-esp8266 3 | // 4 | // by @jpeletier - Epic Labs, 2022 5 | 6 | #include "modbus_server.h" 7 | 8 | #define TAG "ModbusServer" 9 | 10 | namespace esphome { 11 | namespace modbus_server { 12 | 13 | ModbusServer::ModbusServer() {} 14 | 15 | uint32_t ModbusServer::baudRate() { return this->parent_->get_baud_rate(); } 16 | 17 | void ModbusServer::setup() { mb.begin(this); } 18 | 19 | void ModbusServer::set_re_pin(GPIOPin *re_pin) { 20 | if (re_pin != nullptr) { 21 | re_pin_ = re_pin; 22 | re_pin_->setup(); 23 | re_pin_->digital_write(LOW); 24 | // ESP_LOGD(TAG, "set_re_pin(): re_pin_ -> LOW"); 25 | } 26 | } 27 | 28 | void ModbusServer::set_de_pin(GPIOPin *de_pin) { 29 | if (de_pin != nullptr) { 30 | de_pin_ = de_pin; 31 | de_pin_->setup(); 32 | de_pin_->digital_write(LOW); 33 | // ESP_LOGD(TAG, "set_de_pin(): de_pin_ -> LOW"); 34 | } 35 | } 36 | 37 | void ModbusServer::set_address(uint8_t address) { mb.slave(address); } 38 | 39 | bool ModbusServer::add_holding_register(uint16_t start_address, uint16_t value, uint16_t numregs) { 40 | return mb.addHreg(start_address, value, numregs); 41 | } 42 | 43 | bool ModbusServer::add_input_register(uint16_t start_address, uint16_t value, uint16_t numregs) { 44 | return mb.addIreg(start_address, value, numregs); 45 | } 46 | 47 | bool ModbusServer::write_holding_register(uint16_t address, uint16_t value) { return mb.Hreg(address, value); } 48 | 49 | bool ModbusServer::write_input_register(uint16_t address, uint16_t value) { return mb.Ireg(address, value); } 50 | 51 | uint16_t ModbusServer::read_holding_register(uint16_t address) { return mb.Hreg(address); } 52 | 53 | uint16_t ModbusServer::read_input_register(uint16_t address) { return mb.Ireg(address); } 54 | 55 | void ModbusServer::on_read_holding_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs) { 56 | mb.onGet( 57 | HREG(address), [cb](TRegister *reg, uint16_t val) -> uint16_t { return cb(reg->address.address, val); }, numregs); 58 | } 59 | 60 | void ModbusServer::on_read_input_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs) { 61 | mb.onGet( 62 | IREG(address), [cb](TRegister *reg, uint16_t val) -> uint16_t { return cb(reg->address.address, val); }, numregs); 63 | } 64 | 65 | void ModbusServer::on_write_holding_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs) { 66 | mb.onSet( 67 | HREG(address), [cb](TRegister *reg, uint16_t val) -> uint16_t { return cb(reg->address.address, val); }, numregs); 68 | } 69 | 70 | void ModbusServer::on_write_input_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs) { 71 | mb.onSet( 72 | IREG(address), [cb](TRegister *reg, uint16_t val) -> uint16_t { return cb(reg->address.address, val); }, numregs); 73 | } 74 | 75 | 76 | // Stream class implementation: 77 | size_t ModbusServer::write(uint8_t data) { 78 | if (( (re_pin_ != nullptr) || (de_pin_ != nullptr) ) && !sending) { 79 | if (re_pin_ != nullptr) 80 | re_pin_->digital_write(HIGH); 81 | // ESP_LOGV(TAG, "write(): re_pin_ -> HIGH"); 82 | if (de_pin_ != nullptr) 83 | de_pin_->digital_write(HIGH); 84 | // ESP_LOGV(TAG, "write(): de_pin_ -> HIGH"); 85 | sending = true; 86 | } 87 | return uart::UARTDevice::write(data); 88 | } 89 | int ModbusServer::available() { return uart::UARTDevice::available(); } 90 | int ModbusServer::read() { return uart::UARTDevice::read(); } 91 | int ModbusServer::peek() { return uart::UARTDevice::peek(); } 92 | void ModbusServer::flush() { 93 | uart::UARTDevice::flush(); 94 | if (( (re_pin_ != nullptr) || (de_pin_ != nullptr) ) && sending) { 95 | if (re_pin_ != nullptr) 96 | re_pin_->digital_write(LOW); 97 | // ESP_LOGV(TAG, "flush(): re_pin_ -> LOW"); 98 | if (de_pin_ != nullptr) 99 | de_pin_->digital_write(LOW); 100 | // ESP_LOGV(TAG, "flush(): de_pin_ -> LOW"); 101 | sending = false; 102 | } 103 | } 104 | 105 | void ModbusServer::loop() { mb.task(); }; 106 | } // namespace modbus_server 107 | 108 | } // namespace esphome 109 | -------------------------------------------------------------------------------- /esphome/components/modbus_server/modbus_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This ESPHome component wraps around the modbus-esp8266 by @emelianov: 4 | // https://github.com/emelianov/modbus-esp8266 5 | // 6 | // by @jpeletier - Epic Labs, 2022 7 | 8 | #include "esphome/core/component.h" 9 | #include "esphome/core/entity_base.h" 10 | #include "esphome/core/preferences.h" 11 | #include "esphome/core/helpers.h" 12 | #include "esphome/components/uart/uart.h" 13 | #include 14 | #include 15 | 16 | namespace esphome { 17 | namespace modbus_server { 18 | using namespace std; 19 | 20 | // callback for read and write lambdas 21 | typedef std::function cbOnReadWrite; 22 | 23 | class ModbusServer : public esphome::uart::UARTDevice, public Component, public Stream { 24 | public: 25 | explicit ModbusServer(); 26 | 27 | void setup() override; 28 | void loop() override; 29 | uint32_t baudRate(); // for compatibility with ModbusRTU lib 30 | /// @brief Sets the slave address for this instance 31 | /// @param address the slave address this instance will respond as 32 | void set_address(uint8_t address); 33 | 34 | /// @brief Sets the DE pin and toggles it on read/write 35 | /// @param pin_de is the pin to set LOW when transmitting 36 | void set_de_pin(GPIOPin *de_pin); 37 | 38 | /// @brief Sets the RE pin and toggles it on read/write 39 | /// @param pin_re is the pin to set LOW when reading 40 | void set_re_pin(GPIOPin *re_pin); 41 | 42 | /// @brief Adds a new range of holding registers 43 | /// @param start_address Address of the first register 44 | /// @param value Default value for the registers 45 | /// @param numregs Number of registers in the range 46 | /// @return true if successful 47 | bool add_holding_register(uint16_t start_address, uint16_t value, uint16_t numregs = 1); 48 | 49 | /// @brief Adds a new range of input registers 50 | /// @param start_address Address of the first register 51 | /// @param value Default value for the registers 52 | /// @param numregs Number of registers in the range 53 | /// @return true if successful 54 | bool add_input_register(uint16_t start_address, uint16_t value, uint16_t numregs = 1); 55 | 56 | /// @brief Sets a holding register to the specified value 57 | /// @param address Address of the register 58 | /// @param value New value for the register 59 | /// @return true if successful 60 | bool write_holding_register(uint16_t address, uint16_t value); 61 | 62 | /// @brief Sets an input register to the specified value 63 | /// @param address Address of the register 64 | /// @param value New value for the register 65 | /// @return true if successful 66 | bool write_input_register(uint16_t address, uint16_t value); 67 | 68 | /// @brief Retrieves the current value of the given holding register 69 | /// @param address Address of the register to read 70 | /// @return the register value 71 | uint16_t read_holding_register(uint16_t address); 72 | 73 | /// @brief Retrieves the current value of the given input register 74 | /// @param address Address of the register to read 75 | /// @return the register value 76 | uint16_t read_input_register(uint16_t address); 77 | 78 | /// @brief Sets a callback to be invoked when the specified holding register range is read 79 | /// @param address start address of the register range to watch 80 | /// @param cb callback to be called when a register is read. The callback can return a new value to be sent 81 | /// @param numregs number of registers to watch 82 | void on_read_holding_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs = 1); 83 | 84 | /// @brief Sets a callback to be invoked when the specified input register range is read 85 | /// @param address start address of the register range to watch 86 | /// @param cb callback to be called when a register is read. The callback can return a new value to be sent 87 | /// @param numregs number of registers to watch 88 | void on_read_input_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs = 1); 89 | 90 | /// @brief Sets a callback to be invoked when the specified holding register range is written 91 | /// @param address start address of the register range to watch 92 | /// @param cb callback to be called when a register is written 93 | /// @param numregs number of registers to watch 94 | void on_write_holding_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs = 1); 95 | 96 | /// @brief Sets a callback to be invoked when the specified input register range is written 97 | /// @param address start address of the register range to watch 98 | /// @param cb callback to be called when a register is written 99 | /// @param numregs number of registers to watch 100 | void on_write_input_register(uint16_t address, cbOnReadWrite cb, uint16_t numregs = 1); 101 | 102 | // Stream implementation required by ModbusRTU library 103 | size_t write(uint8_t); 104 | int available(); 105 | int read(); 106 | int peek(); 107 | void flush(); 108 | 109 | private: 110 | ModbusRTU mb; // ModbusRTU instance, the man behind the curtain 111 | GPIOPin *re_pin_{nullptr}; 112 | GPIOPin *de_pin_{nullptr}; 113 | bool sending; 114 | }; 115 | 116 | } // namespace modbus_server 117 | } // namespace esphome 118 | -------------------------------------------------------------------------------- /esphome/components/modbus_server/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.cpp_helpers import gpio_pin_expression 4 | from esphome.components import uart 5 | from esphome.const import CONF_ADDRESS, CONF_ID 6 | from esphome import pins 7 | 8 | CONF_START_ADDRESS = "start_address" 9 | CONF_DEFAULT = "default" 10 | CONF_NUMBER = "number" 11 | CONF_RE_PIN = "re_pin" 12 | CONF_DE_PIN = "de_pin" 13 | CONF_ON_READ = "on_read" 14 | CONF_ON_WRITE = "on_write" 15 | 16 | modbus_server_ns = cg.esphome_ns.namespace("modbus_server") 17 | ModbusDeviceComponent = modbus_server_ns.class_("ModbusServer", cg.Component) 18 | 19 | DEPENDENCIES = ["uart"] 20 | 21 | CONFIG_SCHEMA = ( 22 | cv.Schema( 23 | { 24 | cv.GenerateID(): cv.declare_id(ModbusDeviceComponent), 25 | cv.Required(CONF_ADDRESS): cv.positive_int, 26 | cv.Optional(CONF_RE_PIN): pins.gpio_output_pin_schema, 27 | cv.Optional(CONF_DE_PIN): pins.gpio_output_pin_schema, 28 | cv.Optional("holding_registers"): cv.ensure_list( 29 | cv.Schema( 30 | { 31 | cv.Required(CONF_START_ADDRESS): cv.positive_int, 32 | cv.Optional(CONF_DEFAULT, 0): cv.positive_int, 33 | cv.Optional(CONF_NUMBER, 1): cv.positive_int, 34 | cv.Optional(CONF_ON_READ): cv.returning_lambda, 35 | cv.Optional(CONF_ON_WRITE): cv.returning_lambda, 36 | } 37 | ) 38 | ), 39 | cv.Optional("input_registers"): cv.ensure_list( 40 | cv.Schema( 41 | { 42 | cv.Required(CONF_START_ADDRESS): cv.positive_int, 43 | cv.Optional(CONF_DEFAULT, 0): cv.positive_int, 44 | cv.Optional(CONF_NUMBER, 1): cv.positive_int, 45 | cv.Optional(CONF_ON_READ): cv.returning_lambda, 46 | cv.Optional(CONF_ON_WRITE): cv.returning_lambda, 47 | } 48 | ) 49 | ), 50 | } 51 | ) 52 | .extend(uart.UART_DEVICE_SCHEMA) 53 | .extend(cv.COMPONENT_SCHEMA) 54 | ) 55 | 56 | MULTI_CONF = True 57 | CODEOWNERS = ["@jpeletier","@thomase1234"] 58 | 59 | 60 | async def to_code(config): 61 | 62 | cg.add_library("emelianov/modbus-esp8266", "4.1.0") 63 | id = config[CONF_ID] 64 | uart = await cg.get_variable(config["uart_id"]) 65 | server = cg.new_Pvariable(id) 66 | cg.add(server.set_uart_parent(uart)) 67 | cg.add(server.set_address(config[CONF_ADDRESS])) 68 | 69 | if CONF_RE_PIN in config: 70 | pin = await gpio_pin_expression(config[CONF_RE_PIN]) 71 | cg.add(server.set_re_pin(pin)) 72 | if CONF_DE_PIN in config: 73 | pin = await gpio_pin_expression(config[CONF_DE_PIN]) 74 | cg.add(server.set_de_pin(pin)) 75 | 76 | if "holding_registers" in config: 77 | for reg in config["holding_registers"]: 78 | cg.add( 79 | server.add_holding_register( 80 | reg[CONF_START_ADDRESS], reg[CONF_DEFAULT], reg[CONF_NUMBER] 81 | ) 82 | ) 83 | if CONF_ON_READ in reg: 84 | template_ = await cg.process_lambda( 85 | reg[CONF_ON_READ], 86 | [ 87 | (cg.uint16, "address"), 88 | (cg.uint16, "value"), 89 | ], 90 | return_type=cg.uint16, 91 | ) 92 | cg.add( 93 | server.on_read_holding_register( 94 | reg[CONF_START_ADDRESS], template_, reg[CONF_NUMBER] 95 | ) 96 | ) 97 | if CONF_ON_WRITE in reg: 98 | template_ = await cg.process_lambda( 99 | reg[CONF_ON_WRITE], 100 | [ 101 | (cg.uint16, "address"), 102 | (cg.uint16, "value"), 103 | ], 104 | return_type=cg.uint16, 105 | ) 106 | cg.add( 107 | server.on_write_holding_register( 108 | reg[CONF_START_ADDRESS], template_, reg[CONF_NUMBER] 109 | ) 110 | ) 111 | 112 | if "input_registers" in config: 113 | for reg in config["input_registers"]: 114 | cg.add( 115 | server.add_input_register( 116 | reg[CONF_START_ADDRESS], reg[CONF_DEFAULT], reg[CONF_NUMBER] 117 | ) 118 | ) 119 | if CONF_ON_READ in reg: 120 | template_ = await cg.process_lambda( 121 | reg[CONF_ON_READ], 122 | [ 123 | (cg.uint16, "address"), 124 | (cg.uint16, "value"), 125 | ], 126 | return_type=cg.uint16, 127 | ) 128 | cg.add( 129 | server.on_read_input_register( 130 | reg[CONF_START_ADDRESS], template_, reg[CONF_NUMBER] 131 | ) 132 | ) 133 | if CONF_ON_WRITE in reg: 134 | template_ = await cg.process_lambda( 135 | reg[CONF_ON_WRITE], 136 | [ 137 | (cg.uint16, "address"), 138 | (cg.uint16, "value"), 139 | ], 140 | return_type=cg.uint16, 141 | ) 142 | cg.add( 143 | server.on_write_input_register( 144 | reg[CONF_START_ADDRESS], template_, reg[CONF_NUMBER] 145 | ) 146 | ) 147 | 148 | await cg.register_component(server, config) 149 | 150 | return 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | This project allows you to set the charge speed of a Shell Recharge Advanced 3.0 charging point by emulating a Xemex CSMB. 4 | 5 | ## How is this working ? 6 | 7 | ESPhome device acting as a Xemex CSMB by simulating a Modbus RTU Slave/Client/Server that can be polled by a master (e.g. a EV Wallbox like Shell Recharge Advanced 3.0) and delivers IREGs and HREGs which can be controlled arbitrarily. Currently I'm including a Home Assistant automation that uses values from my Shelly 3EM. It's based on the work from [NMOptimization](https://community.home-assistant.io/u/NMOptimization). Originally posted on a thread called [My New Motion integration EV Charging from Shell newmotion](https://community.home-assistant.io/t/my-new-motion-integration-ev-charging-from-shell-newmotion/369593/153) 8 | 9 | The Xemex CSMB is used to measure the current on up to 3 phases. Based on the actual current measurements, the Shell Recharge 3.0 can decrease or increase the power that can be consumed by the connected car. 10 | 11 | ## External Documentation 12 | 13 | ### Documentation on the Xemex CSMB 14 | 15 | https://xemex.eu/products/meters-sensors/csmb/ 16 | https://xemex.eu/wp-content/uploads/2021/07/User-manual-CSMB-1.0.pdf 17 | Page 9 of the user manual contains info on the supported modbus functions. 18 | 19 | ### Install guides of Shell Advanced Recharge 3.0 20 | 21 | https://a.storyblok.com/f/85281/x/e035ddb473/17srqic01_advanced-3-0_quick-installation-guide.pdf 22 | https://my-instructions.com/shellrecharge/advanced-3.0/?locale=en-GB 23 | 24 | ### General Modbus Info 25 | 26 | https://esphome.io/components/modbus_controller.html 27 | https://www.modbustools.com/modbus.html 28 | 29 | ## How to set up the hardware ? 30 | 31 | 1. Connect ESP32 dev board to RS485 module. 32 | 2. Connect RS485 A and B connectors to the A and B wire that goes to the Shell Recharge 3.0 Wallbox. 33 | 3. Build and flash the firmware based on the [sample ESPHome config](/esphome-xemex-fake-modbus-server.yaml). 34 | 4. Add new device in HomeAssistant. 35 | 5. Now you should be able to set the ctXcurrent numbers. When you set them using HomeAssistant, the values are also reported to your Wallbox. 36 | 6. Setup an automation to configure the charge speed. Here you can find a [charge automation](/charge_automation.yaml) 37 | 7. Set the input_number main_maximal_current to the correct value 38 | 39 | ## Interesting info 40 | 41 | - My house has a single phase - 40 A connection to the power grid. The house also has a photovoltaic installation that delivers up to 5kW peak. My automation tries to optime power consumption from the sun. 42 | - [In a sister project](https://github.com/thomase1234/esphome-modbus-client-xemex-csmb), I've pulled all data from my Xemex CSMB. 43 | - The real CSMB has 1 CT connected to CT1. Using my custom modbus client, I could see that the register for CT1 contained the actual current. The other to registers ( CT2 and CT3 ) had a non-changing value, which I took over in the ESPHome config. 44 | - Immediately after booting, the Shell Recharge 3.0 Wallbox first requests the Device Code register (0x4002). It expects '20802' as a response. If not, it'll continue retrying. 45 | - The Shell Recharge 3 Wallbox requests the 3 CT registers every 2 seconds. 46 | - You have to know what the max Current ( in Amps ) setting is on your Wallbox. In my case, this was set to 37.8 Amps. As soon as the fake CSMB starts reporting values higher than 37.8 Amps, my charging would decrease. Set the input_number main_maximal_current to the correct value. 47 | - The wallbox refuses to limit the current to a value lower than 8A. If you try to do so, the wallbox will set its limit to 0A and charging will stop (even if the car supports charging at a lower current). 48 | 49 | ## How to use the Home Assistant automation ? 50 | 51 | Some info on the sensors that I used in the [Automation](/charge_automation.yaml). 52 | 53 | - shell3em_main_current is the current on my cable between grid and house. This number is always positive, even if my house ( thanks to the pv - solar panels ) is producing more electricity than it consumes. If it reads 5 Amp, it can be that my house is consuming 5 Amp from the grid, or it can be that my house is delivering 5 Amp to the grid. 54 | 55 | - shell3em_main_power_factor is the power factor which ranges between -1 and 1. If the number is negative, that means that my house is producing more energy than it consumes which means that the excess energy is delivered to the grid. If the number is positive, the house is consuming from the grid. Usually the value is either very close to 1 when consuming, or very close to -1 when delivering to the grid. 56 | 57 | Multiplying the 2 previous parameters give me a single number with the total of Amps consumed. Negative numbers for delivering. 58 | 59 | - shelly3em_laadpaal_current is the current on the "laadpaal". "Laadpaal" is Dutch for Wall Charger. Since I know that the Wall Charger will always consume energy, I didn't multiply the number with its corresponding power_factor. 60 | 61 | ### The different Charge Modes 62 | 63 | In one of my dashboards I created a dropdown menu which allows me to set the charge mode that is used by the [charge automation](/charge_automation.yaml). 64 | 65 | ![Charge Mode Selector](/pictures/HomeAssistant_ChargeMode.png) 66 | 67 | ``` 68 | type: custom:mushroom-select-card 69 | entity: input_select.charge_mode 70 | secondary_info: last-changed 71 | ``` 72 | 73 | #### Max Speed 74 | 75 | ![Max Speed](/pictures/MaxSpeed.png) 76 | 77 | #### 2A From Grid 78 | 79 | #### Manual Charge Speed 80 | 81 | The Charge speed is set to the value in _input_number.chargepoint_manual_current_ . Note that total consumption can be higher or lower based on other consumers and producers of power. The screemshot was taken at night time, so solar panels didn't produce anything. 82 | 83 | ![Manual Charge Speed](/pictures/ManualChargeSpeed.png) 84 | 85 | #### Manual Max Main 86 | 87 | The total house consumption stays at the value in _input_number.main_manual_current_ . Note that it the charge speed will go up or down based on other electrical consumers ( washing machines, toaster,... ) or producers ( typically solar panels or your in-house fusion reactor ). 88 | 89 | ![Manual Max Main](/pictures/ManualMaxMain.png) 90 | 91 | Note on picture: as you can see, the charging session started as _Max Speed_ 92 | 93 | ## Hardware 94 | 95 | ### Wiring 96 | 97 | Note that this show the wiring that I created. Other configurations are possible. 98 | 99 | Power: I removed the micro-usb plug from an old USB cable and looked for the Ground (GND) and +5V wires. Using a bread board, I made the following connections. 100 | 101 | - connect the GND wire from the USB cable to both a GND Pin on the ESP32 and the GND Pin on the RS485. 102 | - connect the -5V wire from the USB cable to both the 5V Pin on the ESP32 and the VCC Pin on the RS485. 103 | 104 | Data 105 | 106 | - connect a wire between the ESP32 G18 and the RS485 TXD Pin 107 | - connect a wire between the ESP32 G19 and the RS485 RXD Pin 108 | 109 | ### The ESP32 Board I used 110 | 111 | Link to product page on [Azdelivery.de](https://www.az-delivery.de/en/collections/alle-produkte/products/esp32-developmentboard) 112 | 113 | ![ESP32 NODEMCU](/pictures/esp32-nodemcu-module-wlan-wifi-development-board-mit-cp2102-nachfolgermodell-zum-esp8266-kompatibel-mit-arduino-872375_400x.webp) 114 | ![Pinout Diagram](/pictures/ESP_-_32_NodeMCU_Developmentboard_Pinout_Diagram.jpg) 115 | 116 | ### The RS485 module I used 117 | 118 | Link to [amazon.com.be](https://www.amazon.com.be/-/nl/Fasizi-RS485-adapter-seri%C3%ABle-aansluiting/dp/B09Z2GTMJ8/) 119 | 120 | ![RS485-module](/pictures/RS485_Adapter.jpg) 121 | -------------------------------------------------------------------------------- /sample_output/serial_output.log: -------------------------------------------------------------------------------- 1 | INFO ESPHome 2023.6.4 2 | INFO Reading configuration /config/esphome/modbus-server-xemex.yaml... 3 | INFO Updating https://github.com/thomase1234/esphome-fake-xemex-csmb.git@thomas-dev 4 | INFO Detected timezone 'Europe/Brussels' 5 | INFO Starting log output from hostname.domain_redacted using esphome API 6 | INFO Successfully connected to hostname.domain_redacted 7 | [11:25:54][I][app:102]: ESPHome version 2023.6.4 compiled on Jul 7 2023, 11:23:26 8 | [11:25:54][I][app:104]: Project MODBUS.Server-Xemex version 1 9 | [11:25:54][C][wifi:543]: WiFi: 10 | [11:25:54][C][wifi:379]: Local MAC: AA:BB:CC:DD:EE:FF 11 | [11:25:54][C][wifi:380]: SSID: 'xxx'[redacted] 12 | [11:25:54][C][wifi:381]: IP Address: 192.168.6.123 13 | [11:25:54][C][wifi:383]: BSSID: AA:BB:CC:DD:EE:EE[redacted] 14 | [11:25:54][C][wifi:384]: Hostname: 'modbus-server-xemex' 15 | [11:25:54][C][wifi:386]: Signal strength: -68 dB ▂▄▆█ 16 | [11:25:54][C][wifi:390]: Channel: 6 17 | [11:25:54][C][wifi:391]: Subnet: 255.255.255.0 18 | [11:25:54][C][wifi:392]: Gateway: 192.168.6.1 19 | [11:25:54][C][wifi:393]: DNS1: 192.168.6.1 20 | [11:25:54][C][wifi:394]: DNS2: 0.0.0.0 21 | [11:25:54][C][logger:301]: Logger: 22 | [11:25:54][C][logger:302]: Level: DEBUG 23 | [11:25:54][C][logger:303]: Log Baud Rate: 115200 24 | [11:25:54][C][logger:305]: Hardware UART: UART0 25 | [11:25:54][C][uart.arduino_esp32:124]: UART Bus 1: 26 | [11:25:54][C][uart.arduino_esp32:125]: TX Pin: GPIO18 27 | [11:25:54][C][uart.arduino_esp32:126]: RX Pin: GPIO19 28 | [11:25:54][C][uart.arduino_esp32:128]: RX Buffer Size: 256 29 | [11:25:54][C][uart.arduino_esp32:130]: Baud Rate: 9600 baud 30 | [11:25:54][C][uart.arduino_esp32:131]: Data Bits: 8 31 | [11:25:54][C][uart.arduino_esp32:132]: Parity: EVEN 32 | [11:25:54][C][uart.arduino_esp32:133]: Stop bits: 1 33 | [11:25:54][C][template.number:050]: Template Number 'setct1current' 34 | [11:25:54][C][template.number:051]: Optimistic: NO 35 | [11:25:54][C][template.number:052]: Update Interval: 2.0s 36 | [11:25:54][C][template.number:050]: Template Number 'setct2current' 37 | [11:25:54][C][template.number:051]: Optimistic: NO 38 | [11:25:54][C][template.number:052]: Update Interval: 2.0s 39 | [11:25:54][C][template.number:050]: Template Number 'setct3current' 40 | [11:25:54][C][template.number:051]: Optimistic: NO 41 | [11:25:54][C][template.number:052]: Update Interval: 2.0s 42 | [11:25:54][C][version.text_sensor:021]: Version Text Sensor 'modbus-server-xemex - ESPHome Version' 43 | [11:25:54][C][version.text_sensor:021]: Icon: 'mdi:new-box' 44 | [11:25:54][C][homeassistant.time:010]: Home Assistant Time: 45 | [11:25:54][C][homeassistant.time:011]: Timezone: 'CET-1CEST,M3.5.0,M10.5.0/3' 46 | [11:25:54][C][captive_portal:088]: Captive Portal: 47 | [11:25:54][C][web_server:161]: Web Server: 48 | [11:25:54][C][web_server:162]: Address: hostname.domain_redacted:80 49 | [11:25:54][C][mdns:112]: mDNS: 50 | [11:25:54][C][mdns:113]: Hostname: modbus-server-xemex 51 | [11:25:54][C][ota:093]: Over-The-Air Updates: 52 | [11:25:54][C][ota:094]: Address: hostname.domain_redacted:3232 53 | [11:25:54][C][ota:097]: Using Password. 54 | [11:25:54][C][api:138]: API Server: 55 | [11:25:54][C][api:139]: Address: hostname.domain_redacted:6053 56 | [11:25:54][C][api:141]: Using noise encryption: YES 57 | [11:25:54][C][wifi_info:009]: WifiInfo IPAddress 'modbus-server-xemex - IP Address' 58 | [11:25:54][C][wifi_info:011]: WifiInfo SSID 'modbus-server-xemex - Wi-Fi SSID' 59 | [11:25:54][C][wifi_info:012]: WifiInfo BSSID 'modbus-server-xemex - Wi-Fi BSSID' 60 | [11:27:12][I][ON_READ:120]: This is a lambda. address=0x4002, value=20802 61 | [11:27:12][D][uart_debug:114]: <<< 01:03:40:02:00:01:30:0A 62 | [11:27:12][D][uart_debug:114]: >>> 01:03:02:51:42:05:E5 63 | [11:27:16][I][ON_READ:120]: This is a lambda. address=0x4002, value=20802 64 | [11:27:16][D][uart_debug:114]: <<< 01:03:40:02:00:01:30:0A 65 | [11:27:16][D][uart_debug:114]: >>> 01:03:02:51:42:05:E5 66 | [11:27:19][I][ON_READ:120]: This is a lambda. address=0x4002, value=20802 67 | [11:27:19][D][uart_debug:114]: <<< 01:03:40:02:00:01:30:0A 68 | [11:27:19][D][uart_debug:114]: >>> 01:03:02:51:42:05:E5 69 | [11:27:20][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 70 | [11:27:20][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 71 | [11:27:20][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 72 | [11:27:20][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 73 | [11:27:20][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 74 | [11:27:20][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 75 | [11:27:20][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 76 | [11:27:21][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 77 | [11:27:23][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 78 | [11:27:23][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 79 | [11:27:23][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 80 | [11:27:23][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 81 | [11:27:23][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 82 | [11:27:23][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 83 | [11:27:23][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 84 | [11:27:23][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 85 | [11:27:25][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 86 | [11:27:25][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 87 | [11:27:25][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 88 | [11:27:25][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 89 | [11:27:25][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 90 | [11:27:25][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 91 | [11:27:25][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 92 | [11:27:25][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 93 | [11:27:27][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 94 | [11:27:27][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 95 | [11:27:27][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 96 | [11:27:27][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 97 | [11:27:27][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 98 | [11:27:27][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 99 | [11:27:27][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 100 | [11:27:27][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 101 | [11:27:29][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 102 | [11:27:29][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 103 | [11:27:29][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 104 | [11:27:29][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 105 | [11:27:29][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 106 | [11:27:29][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 107 | [11:27:29][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 108 | [11:27:29][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 109 | [11:27:31][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 110 | [11:27:31][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 111 | [11:27:31][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 112 | [11:27:31][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 113 | [11:27:31][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 114 | [11:27:31][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 115 | [11:27:31][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 116 | [11:27:31][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 117 | [11:27:33][I][ON_READ:246]: This is a lambda. address=0x500c, value=0 118 | [11:27:33][I][ON_READ:255]: This is a lambda. address=0x500d, value=0 119 | [11:27:33][I][ON_READ:264]: This is a lambda. address=0x500e, value=0 120 | [11:27:33][I][ON_READ:273]: This is a lambda. address=0x500f, value=0 121 | [11:27:33][I][ON_READ:282]: This is a lambda. address=0x5010, value=0 122 | [11:27:33][I][ON_READ:291]: This is a lambda. address=0x5011, value=0 123 | [11:27:33][D][uart_debug:114]: <<< 01:03:50:0C:00:06:14:CB 124 | [11:27:34][D][uart_debug:114]: >>> 01:03:0C:00:00:00:00:00:00:00:00:00:00:00:00:93:70 125 | -------------------------------------------------------------------------------- /charge_automation.yaml: -------------------------------------------------------------------------------- 1 | alias: Chargepoint Current Regulation 2 | description: "" 3 | trigger: 4 | - platform: time_pattern 5 | seconds: /2 6 | enabled: true 7 | action: 8 | - choose: # start of setting desired current based on charge_mode 9 | - conditions: 10 | - condition: state 11 | entity_id: input_select.charge_mode 12 | state: Max Speed 13 | sequence: 14 | - service: input_number.set_value 15 | data: 16 | value: >- 17 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 18 | {% set pf = 1 %} 19 | {% else %} 20 | {% set pf = -1 %} 21 | {% endif %} 22 | {% set newcurrent = states('input_number.main_maximal_current') | float() - states('sensor.shell3em_main_current') | float() * pf | float() + states('sensor.shelly3em_laadpaal_current') | float() %} 23 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 24 | {{ states('input_number.chargepoint_minimal_current') | float() }} 25 | {%- else -%} 26 | {{ newcurrent }} 27 | {%- endif %} 28 | target: 29 | entity_id: input_number.chargepoint_desired_current 30 | - conditions: 31 | - condition: state 32 | entity_id: input_select.charge_mode 33 | state: Avoid Power To Grid 34 | sequence: 35 | - service: input_number.set_value 36 | data: 37 | value: >- 38 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 39 | {% set pf = 1 %} 40 | {% else %} 41 | {% set pf = -1 %} 42 | {% endif %} 43 | {% set newcurrent = states('sensor.shelly3em_laadpaal_current') | float() - states('sensor.shell3em_main_current') | float() * pf | float() %} 44 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 45 | {{ states('input_number.chargepoint_minimal_current') | float() }} 46 | {%- else -%} 47 | {{ newcurrent }} 48 | {%- endif %} 49 | target: 50 | entity_id: input_number.chargepoint_desired_current 51 | - conditions: 52 | - condition: state 53 | entity_id: input_select.charge_mode 54 | state: 2A From Grid 55 | sequence: 56 | - service: input_number.set_value 57 | data: 58 | value: >- 59 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 60 | {% set pf = 1 %} 61 | {% else %} 62 | {% set pf = -1 %} 63 | {% endif %} 64 | {% set newcurrent = states('sensor.shelly3em_laadpaal_current') | float() - states('sensor.shell3em_main_current') | float() * pf | float() + 2 %} 65 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 66 | {{ states('input_number.chargepoint_minimal_current') | float() }} 67 | {%- else -%} 68 | {{ newcurrent }} 69 | {%- endif %} 70 | target: 71 | entity_id: input_number.chargepoint_desired_current 72 | - conditions: 73 | - condition: state 74 | entity_id: input_select.charge_mode 75 | state: Solar only 76 | sequence: 77 | - service: input_number.set_value 78 | data: 79 | value: >- 80 | {% set newcurrent = states('sensor.sma_grid_phase_1_current') | float() %} 81 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 82 | {{ states('input_number.chargepoint_minimal_current') | float() }} 83 | {%- else -%} 84 | {{ newcurrent }} 85 | {%- endif %} 86 | target: 87 | entity_id: input_number.chargepoint_desired_current 88 | - conditions: 89 | - condition: state 90 | entity_id: input_select.charge_mode 91 | state: Manual Charge Speed 92 | sequence: 93 | - service: input_number.set_value 94 | data: 95 | value: >- 96 | {% set newcurrent = states('input_number.chargepoint_manual_current') | float() %} 97 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 98 | {{ states('input_number.chargepoint_minimal_current') | float() }} 99 | {%- else -%} 100 | {{ newcurrent }} 101 | {%- endif %} 102 | target: 103 | entity_id: input_number.chargepoint_desired_current 104 | - conditions: 105 | - condition: state 106 | entity_id: input_select.charge_mode 107 | state: Manual Max Main 108 | sequence: 109 | - service: input_number.set_value 110 | data: 111 | value: >- 112 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 113 | {% set pf = 1 %} 114 | {% else %} 115 | {% set pf = -1 %} 116 | {% endif %} 117 | {% set newcurrent = states('input_number.main_manual_current') | float() - ( states('sensor.shell3em_main_current') | float() * pf | float() - states('sensor.shelly3em_laadpaal_current') | float() ) %} 118 | {% if newcurrent < states('input_number.chargepoint_minimal_current') | float() -%} 119 | {{ states('input_number.chargepoint_minimal_current') | float() }} 120 | {%- else -%} 121 | {{ newcurrent }} 122 | {%- endif %} 123 | target: 124 | entity_id: input_number.chargepoint_desired_current 125 | - conditions: 126 | - condition: state 127 | entity_id: input_select.charge_mode 128 | state: Charging Off 129 | sequence: 130 | - service: input_number.set_value 131 | data: 132 | value: 0 133 | target: 134 | entity_id: input_number.chargepoint_desired_current 135 | enabled: true 136 | # End of setting desired current based on charge_mode 137 | - choose: # start of check if not over-amping the main fuse 138 | - conditions: 139 | - condition: template 140 | value_template: >- ## check if not over-amping the main fuse 141 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 142 | {% set pf = 1 %} 143 | {% else %} 144 | {% set pf = -1 %} 145 | {% endif %} 146 | {{states('input_number.chargepoint_desired_current') |float() - states('sensor.shelly3em_laadpaal_current') |float() + (states('sensor.shell3em_main_current') |float()) * pf | float() > states('input_number.main_maximal_current') |float() }} 147 | sequence: 148 | - service: input_number.set_value 149 | data: 150 | value: >- 151 | {% if states('sensor.shell3em_main_power_factor') | float() > 0 %} 152 | {% set pf = 1 %} 153 | {% else %} 154 | {% set pf = -1 %} 155 | {% endif %} 156 | {{ states('input_number.main_maximal_current') |float() - states('sensor.shell3em_main_current') |float() * pf | float() + states('sensor.shelly3em_laadpaal_current') |float() }} 157 | target: 158 | entity_id: input_number.chargepoint_desired_current 159 | # - service: logbook.log 160 | # data: 161 | # name: chargepoint 162 | # message: Desired current higher than main_maximal_current 163 | # entity_id: input_number.main_maximal_current 164 | enabled: true 165 | # End of check if not over-amping the main fuse 166 | 167 | - choose: # start of check if increasing or decreasing 168 | - conditions: 169 | - condition: template 170 | value_template: >- ## if load should be increased 171 | {{(states('input_number.chargepoint_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) ) > 0 }} 172 | sequence: 173 | - service: number.set_value 174 | data: 175 | value: >- 176 | {{ states('input_number.main_maximal_current') | float() - (states('input_number.chargepoint_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) )}} 177 | target: 178 | entity_id: number.modbus_server_xemex_ct1current 179 | - conditions: 180 | - condition: template 181 | value_template: >- ## if load should be decreased 182 | {{(states('input_number.chargepoint_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) ) <= 0 }} 183 | sequence: 184 | - service: number.set_value 185 | data: 186 | value: >- 187 | {{ states('input_number.main_maximal_current') | float() - (states('input_number.chargepoint_desired_current') | float() - (states('sensor.shelly3em_laadpaal_current') |float() ) )/2}} 188 | target: 189 | entity_id: number.modbus_server_xemex_ct1current 190 | enabled: true 191 | # End of check if not over-amping the main fuse 192 | 193 | mode: single -------------------------------------------------------------------------------- /esphome-xemex-fake-modbus-server.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: "modbus-server-xemex" 3 | device_comment: "Modbus Client that can simulate modbus of Xemex CSMB" 4 | 5 | esphome: 6 | name: ${device_name} 7 | friendly_name: ${device_name} 8 | comment: $device_comment 9 | name_add_mac_suffix: false 10 | project: 11 | name: "MODBUS.Server-Xemex" 12 | version: "1" 13 | 14 | 15 | esp32: 16 | board: nodemcu-32s 17 | #https://www.az-delivery.de/en/products/esp32-developmentboard 18 | 19 | framework: 20 | type: arduino 21 | 22 | logger: 23 | 24 | api: 25 | encryption: 26 | key: !secret api_encryption_key 27 | 28 | ota: 29 | password: !secret ota_password 30 | 31 | wifi: 32 | ssid: !secret wifi_ssid 33 | password: !secret wifi_password 34 | domain : !secret domain 35 | 36 | ap: 37 | ssid: ${device_name} AP 38 | password: !secret hotspot_pass 39 | 40 | captive_portal: 41 | 42 | # Enable Web server 43 | web_server: 44 | port: 80 45 | 46 | text_sensor: 47 | - platform: wifi_info 48 | ip_address: 49 | name: "${device_name} - IP Address" 50 | ssid: 51 | name: "${device_name} - Wi-Fi SSID" 52 | bssid: 53 | name: "${device_name} - Wi-Fi BSSID" 54 | - platform: version 55 | name: "${device_name} - ESPHome Version" 56 | hide_timestamp: true 57 | 58 | # see: https://esphome.io/components/time.html 59 | time: 60 | - platform: homeassistant 61 | # id: homeassistant_time 62 | 63 | ## end common.yaml 64 | 65 | 66 | external_components: 67 | - source: github://thomase1234/esphome-fake-xemex-csmb@master 68 | refresh: 60s 69 | components: 70 | - modbus_server 71 | 72 | uart: 73 | - id: intmodbus 74 | # https://cdn.shopify.com/s/files/1/1509/1638/files/ESP-32_NodeMCU_Developmentboard_Pinout.pdf 75 | tx_pin: 18 # DI PURPLE wire 76 | rx_pin: 19 # RO BLUE wire 77 | baud_rate: 9600 # default for Xemex CMSB 78 | stop_bits: 1 #default to 8E1 79 | data_bits: 8 #default to 8E1 80 | parity: EVEN #default to 8E1 81 | debug: 82 | direction: BOTH 83 | 84 | 85 | modbus_server: 86 | - id: modbusserver 87 | uart_id: intmodbus 88 | address: 1 # slave address 89 | # - If you're using a RS485 like this one, make sure to set the re_pin and de_pin 90 | # http://domoticx.com/wp-content/uploads/2018/01/RS485-module-shield.jpg 91 | # re_pin: GPIO17 # optional 92 | # de_pin: GPIO16 # optional 93 | 94 | holding_registers: 95 | # I've implemented some of the regs found in this PDF: 96 | # https://xemex.eu/wp-content/uploads/2021/07/User-manual-CSMB-1.0.pdf 97 | - start_address: 0x4000 # register for Serial Number 98 | default: 0x1 # This is what my real Xemex CSMB returned 99 | number: 2 # number of registers in the range 100 | on_read: | # called whenever a register in the range is read 101 | // 'address' contains the requested register address 102 | // 'value' contains the stored register value 103 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 104 | return value; // you can return the stored value or something else. 105 | 106 | - start_address: 0x4002 # register for Device Code 107 | default: 20802 # default value for this register 108 | number: 1 # number of registers in the range 109 | on_read: | # called whenever a register in the range is read 110 | // 'address' contains the requested register address 111 | // 'value' contains the stored register value 112 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 113 | return value; // you can return the stored value or something else. 114 | 115 | - start_address: 0x4003 # register for Device Address 116 | default: 0x1 # default value for this register 117 | number: 1 # number of registers in the range 118 | on_read: | # called whenever a register in the range is read 119 | // 'address' contains the requested register address 120 | // 'value' contains the stored register value 121 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 122 | return value; // you can return the stored value or something else. 123 | 124 | - start_address: 0x4004 # register for RS485 Baudrate Low 125 | default: 9600 # default value for this register 126 | number: 1 # number of registers in the range 127 | on_read: | # called whenever a register in the range is read 128 | // 'address' contains the requested register address 129 | // 'value' contains the stored register value 130 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 131 | return value; // you can return the stored value or something else. 132 | 133 | - start_address: 0x4005 # starting register for Protocol Version. This is the first register of a FLOAT 134 | default: 0x3f80 # default value for this register 135 | number: 1 # number of registers in the range 136 | on_read: | # called whenever a register in the range is read 137 | // 'address' contains the requested register address 138 | // 'value' contains the stored register value 139 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 140 | return value; // you can return the stored value or something else. 141 | 142 | - start_address: 0x4006 # ending register for Protocol Version. This is the second register of a FLOAT 143 | default: 0x0000 # default value for this register 144 | number: 1 # number of registers in the range 145 | on_read: | # called whenever a register in the range is read 146 | // 'address' contains the requested register address 147 | // 'value' contains the stored register value 148 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 149 | return value; // you can return the stored value or something else. 150 | 151 | - start_address: 0x4007 # starting register for Software Version. This is the first register of a FLOAT 152 | default: 0x4000 # default value for this register 153 | number: 1 # number of registers in the range 154 | on_read: | # called whenever a register in the range is read 155 | // 'address' contains the requested register address 156 | // 'value' contains the stored register value 157 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 158 | return value; // you can return the stored value or something else. 159 | 160 | - start_address: 0x4008 # ending register for Software Version. This is the second register of a FLOAT 161 | default: 0x0000 # default value for this register 162 | number: 1 # number of registers in the range 163 | on_read: | # called whenever a register in the range is read 164 | // 'address' contains the requested register address 165 | // 'value' contains the stored register value 166 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 167 | return value; // you can return the stored value or something else. 168 | 169 | - start_address: 0x4009 # starting register for Hardware Version. This is the first register of a FLOAT 170 | default: 0x0000 # default value for this register 171 | number: 1 # number of registers in the range 172 | on_read: | # called whenever a register in the range is read 173 | // 'address' contains the requested register address 174 | // 'value' contains the stored register value 175 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 176 | return value; // you can return the stored value or something else. 177 | 178 | - start_address: 0x400A # ending register for Hardware Version. This is the second register of a FLOAT 179 | default: 0x0000 # default value for this register 180 | number: 1 # number of registers in the range 181 | on_read: | # called whenever a register in the range is read 182 | // 'address' contains the requested register address 183 | // 'value' contains the stored register value 184 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 185 | return value; // you can return the stored value or something else. 186 | 187 | - start_address: 0x400B # register for Meter Amps. 188 | default: 80 # default value for this register 189 | number: 1 # number of registers in the range 190 | on_read: | # called whenever a register in the range is read 191 | // 'address' contains the requested register address 192 | // 'value' contains the stored register value 193 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 194 | return value; // you can return the stored value or something else. 195 | 196 | - start_address: 0x400C # register for CT Ratio. 197 | default: 2000 # default value for this register 198 | number: 1 # number of registers in the range 199 | on_read: | # called whenever a register in the range is read 200 | // 'address' contains the requested register address 201 | // 'value' contains the stored register value 202 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 203 | return value; // you can return the stored value or something else. 204 | 205 | - start_address: 0x400D # register for RS485 Line Settings. 206 | default: 36 # default value for this register 207 | number: 1 # number of registers in the range 208 | on_read: | # called whenever a register in the range is read 209 | // 'address' contains the requested register address 210 | // 'value' contains the stored register value 211 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 212 | return value; // you can return the stored value or something else. 213 | 214 | - start_address: 0x400E # register for RS485 Line Termination. 215 | default: 1 # default value for this register 216 | number: 1 # number of registers in the range 217 | on_read: | # called whenever a register in the range is read 218 | // 'address' contains the requested register address 219 | // 'value' contains the stored register value 220 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 221 | return value; // you can return the stored value or something else. 222 | 223 | - start_address: 0x400F # register for RS485 Baudrate High. 224 | default: 0 # default value for this register 225 | number: 1 # number of registers in the range 226 | on_read: | # called whenever a register in the range is read 227 | // 'address' contains the requested register address 228 | // 'value' contains the stored register value 229 | ESP_LOGI("ON_READ", "This is a lambda. address=0x%04x, value=%d", address, value); 230 | return value; // you can return the stored value or something else. 231 | 232 | - start_address: 0x500C # starting register for RMS Current CT1. This is the first register of a FLOAT 233 | default: 0x0000 # default value for this register 234 | number: 1 # number of registers in the range 235 | on_read: | # called whenever a register in the range is read 236 | // 'address' contains the requested register address 237 | // 'value' contains the stored register value 238 | ESP_LOGI("ON_READ", "CT1. address=0x%04x, value=0x%04x", address, value); 239 | return value; // you can return the stored value or something else. 240 | 241 | - start_address: 0x500D # ending register for RMS Current CT1. This is the second register of a FLOAT 242 | default: 0x0000 # default value for this register 243 | number: 1 # number of registers in the range 244 | on_read: | # called whenever a register in the range is read 245 | // 'address' contains the requested register address 246 | // 'value' contains the stored register value 247 | ESP_LOGI("ON_READ", "CT1. address=0x%04x, value=0x%04x", address, value); 248 | return value; // you can return the stored value or something else. 249 | 250 | - start_address: 0x500E # starting register for RMS Current CT2. This is the first register of a FLOAT 251 | default: 0x3E73 # default value for this register 3E:73:43:33 252 | number: 1 # number of registers in the range 253 | on_read: | # called whenever a register in the range is read 254 | // 'address' contains the requested register address 255 | // 'value' contains the stored register value 256 | ESP_LOGI("ON_READ", "CT2. address=0x%04x, value=0x%04x", address, value); 257 | return value; // you can return the stored value or something else. 258 | 259 | - start_address: 0x500F # ending register for RMS Current CT2. This is the second register of a FLOAT 260 | default: 0x4333 # default value for this register 261 | number: 1 # number of registers in the range 262 | on_read: | # called whenever a register in the range is read 263 | // 'address' contains the requested register address 264 | // 'value' contains the stored register value 265 | ESP_LOGI("ON_READ", "CT2. address=0x%04x, value=0x%04x", address, value); 266 | return value; // you can return the stored value or something else. 267 | 268 | - start_address: 0x5010 # starting register for RMS Current CT3. This is the first register of a FLOAT 269 | default: 0x3E09 # default value for this register 3E:09:39:9A 270 | number: 1 # number of registers in the range 271 | on_read: | # called whenever a register in the range is read 272 | // 'address' contains the requested register address 273 | // 'value' contains the stored register value 274 | ESP_LOGI("ON_READ", "CT3. address=0x%04x, value=0x%04x", address, value); 275 | return value; // you can return the stored value or something else. 276 | 277 | - start_address: 0x5011 # ending register for RMS Current CT3. This is the second register of a FLOAT 278 | default: 0x399A # default value for this register 279 | number: 1 # number of registers in the range 280 | on_read: | # called whenever a register in the range is read 281 | // 'address' contains the requested register address 282 | // 'value' contains the stored register value 283 | ESP_LOGI("ON_READ", "CT3. address=0x%04x, value=0x%04x", address, value); 284 | return value; // you can return the stored value or something else. 285 | 286 | number: 287 | - platform: template 288 | name: ct1current 289 | id: ct1current 290 | optimistic: False 291 | update_interval: 2s 292 | min_value: 0 293 | max_value: 60 294 | step: 0.1 295 | unit_of_measurement: A 296 | set_action: 297 | then: 298 | - lambda: |- 299 | ESP_LOGD("modbus-server-xemex", "Current set to: %f", x); 300 | // convert from float to 2 uint16_t. 301 | union 302 | { 303 | uint16_t x_int[2]; 304 | float x_f; 305 | } u; 306 | u.x_f = x; 307 | // setting modbusserver registers 308 | // use https://www.h-schmidt.net/FloatConverter/IEEE754.html to doublecheck conversions 309 | modbusserver->write_holding_register(0x500C,u.x_int[1]); 310 | modbusserver->write_holding_register(0x500D,u.x_int[0]); 311 | ESP_LOGD("modbus-server-xemex", "Registers for CT1 set to: 0x%04x and 0x%04x", u.x_int[1], u.x_int[0]); 312 | // and finally set the value of this number template 313 | id(ct1current).publish_state(x); 314 | - platform: template 315 | name: ct2current 316 | id: ct2current 317 | optimistic: False 318 | update_interval: 2s 319 | min_value: 0 320 | max_value: 60 321 | step: 0.1 322 | unit_of_measurement: A 323 | set_action: 324 | then: 325 | - lambda: |- 326 | ESP_LOGD("modbus-server-xemex", "Current set to: %f", x); 327 | // convert from float to 2 uint16_t. 328 | union 329 | { 330 | uint16_t x_int[2]; 331 | float x_f; 332 | } u; 333 | u.x_f = x; 334 | // setting modbusserver registers 335 | // use https://www.h-schmidt.net/FloatConverter/IEEE754.html to doublecheck conversions 336 | modbusserver->write_holding_register(0x500E,u.x_int[1]); 337 | modbusserver->write_holding_register(0x500F,u.x_int[0]); 338 | ESP_LOGD("modbus-server-xemex", "Registers for CT2 set to: 0x%04x and 0x%04x", u.x_int[1], u.x_int[0]); 339 | // and finally set the value of this number template 340 | id(ct2current).publish_state(x); 341 | - platform: template 342 | name: ct3current 343 | id: ct3current 344 | optimistic: False 345 | update_interval: 2s 346 | min_value: 0 347 | max_value: 60 348 | step: 0.1 349 | unit_of_measurement: A 350 | set_action: 351 | then: 352 | - lambda: |- 353 | ESP_LOGD("modbus-server-xemex", "Current set to: %f", x); 354 | // convert from float to 2 uint16_t. 355 | union 356 | { 357 | uint16_t x_int[2]; 358 | float x_f; 359 | } u; 360 | u.x_f = x; 361 | // setting modbusserver registers 362 | // use https://www.h-schmidt.net/FloatConverter/IEEE754.html to doublecheck conversions 363 | modbusserver->write_holding_register(0x5010,u.x_int[1]); 364 | modbusserver->write_holding_register(0x5011,u.x_int[0]); 365 | ESP_LOGD("modbus-server-xemex", "Registers for CT3 set to: 0x%04x and 0x%04x", u.x_int[1], u.x_int[0]); 366 | // and finally set the value of this number template 367 | id(ct3current).publish_state(x); 368 | --------------------------------------------------------------------------------