├── .gitignore ├── README.md ├── components ├── beam_binary_sensor │ ├── __init__.py │ ├── beam_binary_sensor.cpp │ └── beam_binary_sensor.h └── solis_s5 │ ├── __init__.py │ ├── solis_s5.cpp │ └── solis_s5.h ├── solis_piggyback_schematic_0.pdf ├── soliss5-32.yaml ├── soliss5.yaml ├── tools └── soliss5_protocoltest.py └── watermeter.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | /secrets.yaml 6 | *__pycache__* 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esphome-externalcomponents 2 | Some external components for [esphome](https://esphome.io/index.html). Please refer to the esphome website for information about how esphome works, and how to set up and flash devices with esphome. 3 | ## Usage 4 | [See here for how to use external components](https://esphome.io/components/external_components.html). 5 | I've included some examples also. 6 | ## beam_binary_sensor 7 | Intended for infra-red (IR) sensors that include both an led and sensor (e.g. beam sensors,reflective sensors, etc.), e.g. [TCTR5000](https://www.vishay.com/docs/83760/tcrt5000.pdf), [RPR220](https://fscdn.rohm.com/en/products/databook/datasheet/opto/optical_sensor/photosensor/rpr-220.pdf), etc. In particular, may be helpful for optical sensors where ambient light might make erroneous readings (e.g. rotating dials on gas and water meters). 8 | 9 | Beam binary sensor will alternate an output 'driver' (e.g. ir led) `ON` and `OFF`, and only publish `ON` if: 10 | 1. Sensor is `ON` when Driver is `ON` 11 | 2. Sensor is `OFF` when Driver is `OFF` 12 | This ensures that the sensor is not activated erroneously by ambient light (e.g. sunlight). 13 | 14 | Frequency on my device is about 30Hz; this will vary depending on how much other code you have on the device (it's using the loop). This frequency is about half the polling frequency of a normal gpio binary sensor. If you want to pick up much shorter duration pulses, this might not be capable - you may be better off using a regular [pulse_counter](https://esphome.io/components/sensor/pulse_counter.html) instead. 15 | 16 | Usage is similar to a [gpio_binary_sensor](https://esphome.io/components/binary_sensor/gpio.html). Supports all the configuration [binary_sensor](https://esphome.io/components/binary_sensor/index.html) supports. 17 | 18 | | Config | Required? | Type | Default | Description | 19 | | --- | --- | --- | --- | --- | 20 | | `pin_sensor` | required | pin_schema | | pin for the sensor input; as per 'pin' configuration of gpio_binary_sensor | 21 | | `pin_driver` | required | pin_schema | | pin for the output led (or similar) | 22 | | `pin_output` | optional | pin_schema | none | pin to output the state of the beam sensor directly (e.g. for a local led) | 23 | 24 | ## solis_s5 25 | For connection of esphome to a Ginlong Solis S5 solar inverter using the RS485 COM port. I wanted to connect the inverter to Home Assistant without using a cloud service (i.e. on the local network), to minimise delay and dependence on the Solis API and internet connection being functional. However, my solar installer uses the cloud service for warranty and support, so I wanted to keep this data being delivered. The Solis wifi dongle software does not provide reliable means of adding an additional server connection, so I needed to bypass it / work in parallel. I love ESPhome, so here we are. 26 | 27 | Currently, this component is designed only to listen-in to the messages sent from the inverter to the wifi dongle containing operational data. It REQUIRES the Solis wifi stick to function. In future, I would like to extend this to allow a full replacement of the solis wifi stick with the esphome device by transmitting the simple request message the dongle makes periodically. 28 | 29 | After some effort reverse-engineering the protocol, we can obtain most primary operational values from the inverter at a reasonable frequency (looks like about 1/min). 30 | 31 | Config is relatively self-explanatory, please see the example file soliss5.yaml. This shows all the sensors you can configure. All sensors are optional (delete them if you don't want them). Each is a `sensor` schema, so all options from [base sensor configuration](https://esphome.io/components/sensor/index.html#base-sensor-configuration) can be used. You can also configure a polling interval (default is 60s), and the uart port to use (by id). 32 | 33 | | Config | Required? | Type | Default | Description | 34 | | --- | --- | --- | --- | --- | 35 | | `uart_id` | optional | `id` | `UART0` | uart bus to read from; default is UART0 (pin1/3) | 36 | | `update_interval` | optional | time | `'60s'` | minimum interval for reporting values. only sends the last received state each period. set to 1s to receive everything | 37 | | `voltage_dc_1` | optional | `sensor_schema` | none | Inverter DC Voltage 1 | 38 | | `voltage_dc_2` | optional | `sensor_schema` | none | Inverter DC Voltage 2 | 39 | | ... etc ... | | | | see example config [soliss5.yaml](https://github.com/grob6000/esphome-externalcomponents/blob/master/soliss5.yaml) for full list of sensors| 40 | 41 | ### Hardware: 42 | 1. Node-MCU / Wemos or similar ESP8266 or ESP32 device 43 | 2. RS485:TTL serial converter 44 | 3. Solis Wifi dongle 45 | 46 | Connect as per [schematic](https://github.com/grob6000/esphome-externalcomponents/blob/master/solis_piggyback_schematic_0.pdf). 47 | 48 | I did the following: 49 | 1. Assembled and connected the esp and rs485 modules together & wrapped with insulation tape. 50 | 2. Soldered wires between this and the back of the 4-pin JST connector on the solis wifi stick board. 51 | 3. Placing this against the back of the solis wifi stick board, the whole assembly could be reinstalled in the original case. 52 | -------------------------------------------------------------------------------- /components/beam_binary_sensor/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import pins 4 | from esphome.components import binary_sensor 5 | from esphome.const import CONF_PIN, CONF_ID 6 | 7 | beambinarysensor_ns = cg.esphome_ns.namespace('beam_binary_sensor') 8 | BeamBinarySensor = beambinarysensor_ns.class_('BeamBinarySensor', binary_sensor.BinarySensor, cg.Component) 9 | 10 | DEPENDENCIES = [] 11 | AUTO_LOAD = ['binary_sensor'] 12 | 13 | CONF_PIN_SENSOR = "pin_sensor" 14 | CONF_PIN_DRIVER = "pin_driver" 15 | CONF_PIN_OUTPUT = "pin_output" 16 | 17 | CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ 18 | cv.GenerateID(): cv.declare_id(BeamBinarySensor), 19 | cv.Required(CONF_PIN_SENSOR): pins.gpio_input_pin_schema, 20 | cv.Required(CONF_PIN_DRIVER): pins.gpio_output_pin_schema, 21 | cv.Optional(CONF_PIN_OUTPUT): pins.gpio_output_pin_schema, 22 | }).extend(cv.COMPONENT_SCHEMA) 23 | 24 | async def to_code(config): 25 | var = await binary_sensor.new_binary_sensor(config) 26 | await cg.register_component(var, config) 27 | 28 | p = await cg.gpio_pin_expression(config[CONF_PIN_SENSOR]) 29 | cg.add(var.set_pin_sensor(p)) 30 | 31 | p = await cg.gpio_pin_expression(config[CONF_PIN_DRIVER]) 32 | cg.add(var.set_pin_driver(p)) 33 | 34 | if CONF_PIN_OUTPUT in config: 35 | p = await cg.gpio_pin_expression(config[CONF_PIN_OUTPUT]) 36 | cg.add(var.set_pin_output(p)) -------------------------------------------------------------------------------- /components/beam_binary_sensor/beam_binary_sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "beam_binary_sensor.h" 2 | #include "esphome/core/log.h" 3 | 4 | namespace esphome { 5 | namespace beam_binary_sensor { 6 | 7 | static const char *const TAG = "beam_binary_sensor"; 8 | static const uint8_t CYCLECOUNT = 2; // two-stroke 9 | 10 | void BeamBinarySensor::setup() { 11 | this->pin_sensor->setup(); 12 | this->pin_driver->setup(); 13 | this->pin_driver->digital_write(false); // driver off initially 14 | if (this->pin_output != nullptr) { // if configured 15 | this->pin_output->setup(); 16 | this->pin_output->digital_write(false); // output off initially 17 | } 18 | //this->publish_initial_state(false); 19 | } 20 | 21 | void BeamBinarySensor::dump_config() { 22 | LOG_BINARY_SENSOR("", "Beam Binary Sensor", this); 23 | LOG_PIN(" Sensor Pin: ", this->pin_sensor); 24 | LOG_PIN(" Driver Pin: ", this->pin_driver); 25 | LOG_PIN(" Output Pin: ", this->pin_output); // note this checks for null internally 26 | } 27 | 28 | void BeamBinarySensor::loop() { 29 | static uint8_t cyclestate = 0; // keep track of where in the cycle we are 30 | static bool v = true; // state is invalidated if input is not consistent with beam detection, per cycle 31 | switch (cyclestate) { 32 | case 0: 33 | // read sensor (if not low, then value is false - ambient effect) 34 | if (this->pin_sensor->digital_read()) { 35 | v = false; 36 | } 37 | // drive output 38 | this->pin_driver->digital_write(true); 39 | break; 40 | case 1: 41 | // read sensor (if not high, then value is false - no detection) 42 | if (!(this->pin_sensor->digital_read())) { 43 | v = false; 44 | } 45 | // publish state based on last cycle 46 | this->publish_state(v); 47 | // update output pin (if configured) to match 48 | if (this->pin_output != nullptr) { 49 | this->pin_output->digital_write(v); 50 | } 51 | // release driver 52 | this->pin_driver->digital_write(false); // release driver 53 | v = true; // reset value for next cycle 54 | break; 55 | default: 56 | // reset if confused 57 | this->pin_driver->digital_write(false); 58 | v = true; 59 | cyclestate = 0; 60 | break; 61 | } 62 | // progress through cycle 63 | cyclestate++; 64 | cyclestate%=CYCLECOUNT; 65 | 66 | } 67 | 68 | float BeamBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } 69 | 70 | } // namespace gpio 71 | } // namespace esphome -------------------------------------------------------------------------------- /components/beam_binary_sensor/beam_binary_sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/hal.h" 5 | #include "esphome/components/binary_sensor/binary_sensor.h" 6 | 7 | namespace esphome { 8 | namespace beam_binary_sensor { 9 | 10 | class BeamBinarySensor : public binary_sensor::BinarySensor, public Component { 11 | public: 12 | void set_pin_sensor(GPIOPin *pin) { pin_sensor = pin; } 13 | void set_pin_driver(GPIOPin *pin) { pin_driver = pin; } 14 | void set_pin_output(GPIOPin *pin) { pin_output = pin; } 15 | // ========== INTERNAL METHODS ========== 16 | // (In most use cases you won't need these) 17 | /// Setup pin 18 | void setup() override; 19 | void dump_config() override; 20 | /// Hardware priority 21 | float get_setup_priority() const override; 22 | /// Check sensor 23 | void loop() override; 24 | 25 | protected: 26 | GPIOPin *pin_sensor; 27 | GPIOPin *pin_driver; 28 | GPIOPin *pin_output; 29 | }; 30 | 31 | } // namespace gpio 32 | } // namespace esphome -------------------------------------------------------------------------------- /components/solis_s5/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor, binary_sensor, text_sensor, uart 4 | from esphome.const import * 5 | solis_s5_ns = cg.esphome_ns.namespace('solis_s5') 6 | SolisS5Component = solis_s5_ns.class_('SolisS5Component', cg.PollingComponent) 7 | 8 | DEPENDENCIES = ['uart'] 9 | AUTO_LOAD = ['uart', 'sensor', 'text_sensor', 'binary_sensor'] 10 | 11 | CONF_VDC_1 = "voltage_dc_1" 12 | CONF_IDC_1 = "current_dc_1" 13 | CONF_PDC_1 = "power_dc_1" 14 | 15 | CONF_VDC_2 = "voltage_dc_2" 16 | CONF_IDC_2 = "current_dc_2" 17 | CONF_PDC_2 = "power_dc_2" 18 | 19 | CONF_VAC_U = "voltage_ac_u" 20 | CONF_IAC_U = "current_ac_u" 21 | CONF_VAC_V = "voltage_ac_v" 22 | CONF_IAC_V = "current_ac_v" 23 | CONF_VAC_W = "voltage_ac_w" 24 | CONF_IAC_W = "current_ac_w" 25 | CONF_FAC = "frequency" 26 | CONF_PAC_TOTAL = "power_ac_total" 27 | CONF_VAAC_TOTAL = "va_ac_total" 28 | CONF_PFAC = "powerfactor_ac" 29 | 30 | CONF_E_DAY = "energy_today" 31 | CONF_E_MONTH = "energy_thismonth" 32 | CONF_E_TOTAL = "energy_total" 33 | 34 | CONF_T_IGBT = "temperature_igbt" 35 | 36 | CONFIG_SCHEMA = cv.Schema({ 37 | cv.GenerateID(): cv.declare_id(SolisS5Component), 38 | 39 | cv.Optional(CONF_VDC_1): 40 | sensor.sensor_schema(device_class=DEVICE_CLASS_VOLTAGE,unit_of_measurement=UNIT_VOLT,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 41 | cv.Optional(CONF_VDC_2): 42 | sensor.sensor_schema(device_class=DEVICE_CLASS_VOLTAGE,unit_of_measurement=UNIT_VOLT,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 43 | cv.Optional(CONF_VAC_U): 44 | sensor.sensor_schema(device_class=DEVICE_CLASS_VOLTAGE,unit_of_measurement=UNIT_VOLT,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 45 | cv.Optional(CONF_VAC_V): 46 | sensor.sensor_schema(device_class=DEVICE_CLASS_VOLTAGE,unit_of_measurement=UNIT_VOLT,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 47 | cv.Optional(CONF_VAC_W): 48 | sensor.sensor_schema(device_class=DEVICE_CLASS_VOLTAGE,unit_of_measurement=UNIT_VOLT,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 49 | 50 | cv.Optional(CONF_IDC_1): 51 | sensor.sensor_schema(device_class=DEVICE_CLASS_CURRENT,unit_of_measurement=UNIT_AMPERE,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 52 | cv.Optional(CONF_IDC_2): 53 | sensor.sensor_schema(device_class=DEVICE_CLASS_CURRENT,unit_of_measurement=UNIT_AMPERE,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 54 | cv.Optional(CONF_IAC_U): 55 | sensor.sensor_schema(device_class=DEVICE_CLASS_CURRENT,unit_of_measurement=UNIT_AMPERE,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 56 | cv.Optional(CONF_IAC_V): 57 | sensor.sensor_schema(device_class=DEVICE_CLASS_CURRENT,unit_of_measurement=UNIT_AMPERE,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 58 | cv.Optional(CONF_IAC_W): 59 | sensor.sensor_schema(device_class=DEVICE_CLASS_CURRENT,unit_of_measurement=UNIT_AMPERE,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 60 | 61 | cv.Optional(CONF_PDC_1): 62 | sensor.sensor_schema(device_class=DEVICE_CLASS_POWER,unit_of_measurement=UNIT_WATT,accuracy_decimals=0,state_class=STATE_CLASS_MEASUREMENT).extend(), 63 | cv.Optional(CONF_PDC_2): 64 | sensor.sensor_schema(device_class=DEVICE_CLASS_POWER,unit_of_measurement=UNIT_WATT,accuracy_decimals=0,state_class=STATE_CLASS_MEASUREMENT).extend(), 65 | cv.Optional(CONF_PAC_TOTAL): 66 | sensor.sensor_schema(device_class=DEVICE_CLASS_POWER,unit_of_measurement=UNIT_WATT,accuracy_decimals=0,state_class=STATE_CLASS_MEASUREMENT).extend(), 67 | cv.Optional(CONF_VAAC_TOTAL): 68 | sensor.sensor_schema(device_class=DEVICE_CLASS_APPARENT_POWER,unit_of_measurement=UNIT_VOLT_AMPS,accuracy_decimals=0,state_class=STATE_CLASS_MEASUREMENT).extend(), 69 | cv.Optional(CONF_PFAC): 70 | sensor.sensor_schema(device_class=DEVICE_CLASS_POWER_FACTOR,accuracy_decimals=3,state_class=STATE_CLASS_MEASUREMENT).extend(), 71 | 72 | cv.Optional(CONF_E_DAY): 73 | sensor.sensor_schema(device_class=DEVICE_CLASS_ENERGY,unit_of_measurement=UNIT_KILOWATT_HOURS,accuracy_decimals=1,state_class=STATE_CLASS_TOTAL_INCREASING).extend(), 74 | cv.Optional(CONF_E_MONTH): 75 | sensor.sensor_schema(device_class=DEVICE_CLASS_ENERGY,unit_of_measurement=UNIT_KILOWATT_HOURS,accuracy_decimals=0,state_class=STATE_CLASS_TOTAL_INCREASING).extend(), 76 | cv.Optional(CONF_E_TOTAL): 77 | sensor.sensor_schema(device_class=DEVICE_CLASS_ENERGY,unit_of_measurement=UNIT_KILOWATT_HOURS,accuracy_decimals=0,state_class=STATE_CLASS_TOTAL_INCREASING).extend(), 78 | 79 | cv.Optional(CONF_T_IGBT): 80 | sensor.sensor_schema(device_class=DEVICE_CLASS_TEMPERATURE,unit_of_measurement=UNIT_CELSIUS,accuracy_decimals=1,state_class=STATE_CLASS_MEASUREMENT).extend(), 81 | 82 | }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) 83 | 84 | def to_code(config): 85 | var = cg.new_Pvariable(config[CONF_ID]) 86 | yield cg.register_component(var, config) 87 | yield uart.register_uart_device(var, config) 88 | 89 | if CONF_VDC_1 in config: 90 | conf = config[CONF_VDC_1] 91 | sens = yield sensor.new_sensor(conf) 92 | cg.add(var.set_vdc_1_sensor(sens)) 93 | 94 | if CONF_VDC_2 in config: 95 | conf = config[CONF_VDC_2] 96 | sens = yield sensor.new_sensor(conf) 97 | cg.add(var.set_vdc_2_sensor(sens)) 98 | 99 | if CONF_VAC_U in config: 100 | conf = config[CONF_VAC_U] 101 | sens = yield sensor.new_sensor(conf) 102 | cg.add(var.set_vac_u_sensor(sens)) 103 | 104 | if CONF_VAC_V in config: 105 | conf = config[CONF_VAC_V] 106 | sens = yield sensor.new_sensor(conf) 107 | cg.add(var.set_vac_v_sensor(sens)) 108 | 109 | if CONF_VAC_W in config: 110 | conf = config[CONF_VAC_W] 111 | sens = yield sensor.new_sensor(conf) 112 | cg.add(var.set_vac_w_sensor(sens)) 113 | 114 | 115 | 116 | if CONF_IDC_1 in config: 117 | conf = config[CONF_IDC_1] 118 | sens = yield sensor.new_sensor(conf) 119 | cg.add(var.set_idc_1_sensor(sens)) 120 | 121 | if CONF_IDC_2 in config: 122 | conf = config[CONF_IDC_2] 123 | sens = yield sensor.new_sensor(conf) 124 | cg.add(var.set_idc_2_sensor(sens)) 125 | 126 | if CONF_IAC_U in config: 127 | conf = config[CONF_IAC_U] 128 | sens = yield sensor.new_sensor(conf) 129 | cg.add(var.set_iac_u_sensor(sens)) 130 | 131 | if CONF_IAC_V in config: 132 | conf = config[CONF_IAC_V] 133 | sens = yield sensor.new_sensor(conf) 134 | cg.add(var.set_iac_v_sensor(sens)) 135 | 136 | if CONF_IAC_W in config: 137 | conf = config[CONF_IAC_W] 138 | sens = yield sensor.new_sensor(conf) 139 | cg.add(var.set_iac_w_sensor(sens)) 140 | 141 | 142 | 143 | if CONF_PDC_1 in config: 144 | conf = config[CONF_PDC_1] 145 | sens = yield sensor.new_sensor(conf) 146 | cg.add(var.set_pdc_1_sensor(sens)) 147 | 148 | if CONF_PDC_2 in config: 149 | conf = config[CONF_PDC_2] 150 | sens = yield sensor.new_sensor(conf) 151 | cg.add(var.set_pdc_2_sensor(sens)) 152 | 153 | if CONF_PAC_TOTAL in config: 154 | conf = config[CONF_PAC_TOTAL] 155 | sens = yield sensor.new_sensor(conf) 156 | cg.add(var.set_pac_total_sensor(sens)) 157 | 158 | if CONF_VAAC_TOTAL in config: 159 | conf = config[CONF_VAAC_TOTAL] 160 | sens = yield sensor.new_sensor(conf) 161 | cg.add(var.set_vaac_total_sensor(sens)) 162 | 163 | if CONF_PFAC in config: 164 | conf = config[CONF_PFAC] 165 | sens = yield sensor.new_sensor(conf) 166 | cg.add(var.set_pfac_sensor(sens)) 167 | 168 | 169 | 170 | if CONF_E_DAY in config: 171 | conf = config[CONF_E_DAY] 172 | sens = yield sensor.new_sensor(conf) 173 | cg.add(var.set_e_day_sensor(sens)) 174 | 175 | if CONF_E_MONTH in config: 176 | conf = config[CONF_E_MONTH] 177 | sens = yield sensor.new_sensor(conf) 178 | cg.add(var.set_e_month_sensor(sens)) 179 | 180 | if CONF_E_TOTAL in config: 181 | conf = config[CONF_E_TOTAL] 182 | sens = yield sensor.new_sensor(conf) 183 | cg.add(var.set_e_total_sensor(sens)) 184 | 185 | 186 | 187 | if CONF_T_IGBT in config: 188 | conf = config[CONF_T_IGBT] 189 | sens = yield sensor.new_sensor(conf) 190 | cg.add(var.set_t_igbt_sensor(sens)) -------------------------------------------------------------------------------- /components/solis_s5/solis_s5.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/component.h" 2 | #include "esphome/components/sensor/sensor.h" 3 | //#include "esphome/components/text_sensor/text_sensor.h" 4 | //#include "esphome/components/binary_sensor/binary_sensor.h" 5 | #include "esphome/components/uart/uart.h" 6 | #include "esphome/core/log.h" 7 | #include "solis_s5.h" 8 | 9 | namespace esphome { 10 | namespace solis_s5 { 11 | 12 | static const char *TAG = "solis_s5"; 13 | 14 | void SolisS5Component::setup() { 15 | // nothing to do here 16 | } 17 | 18 | void SolisS5Component::loop() { 19 | 20 | if (this->sensorupdateprogress > 0) { 21 | switch (this->sensorupdateprogress) { 22 | case 19: 23 | if (this->vdc1sensor != nullptr) { 24 | uint16_t v = this->messagedata[4] + this->messagedata[5]*256; 25 | this->vdc1sensor->publish_state((float)v * 0.1f); 26 | } 27 | break; 28 | case 18: 29 | if (this->vdc2sensor != nullptr) { 30 | uint16_t v = this->messagedata[28] + this->messagedata[29]*256; 31 | this->vdc2sensor->publish_state((float)v * 0.1f); 32 | } 33 | break; 34 | case 17: 35 | if (this->vacusensor != nullptr) { 36 | uint16_t v = this->messagedata[8] + this->messagedata[9]*256; 37 | this->vacusensor->publish_state((float)v * 0.1f); 38 | } 39 | break; 40 | case 16: 41 | if (this->vacvsensor != nullptr) { 42 | uint16_t v = this->messagedata[74] + this->messagedata[75]*256; 43 | this->vacvsensor->publish_state((float)v * 0.1f); 44 | } 45 | break; 46 | case 15: 47 | if (this->vacwsensor != nullptr) { 48 | uint16_t v = this->messagedata[70] + this->messagedata[71]*256; 49 | this->vacwsensor->publish_state((float)v * 0.1f); 50 | } 51 | break; 52 | case 14: 53 | if (this->idc1sensor != nullptr) { 54 | uint16_t v = this->messagedata[6] + this->messagedata[7]*256; 55 | this->idc1sensor->publish_state((float)v * 0.1f); 56 | } 57 | break; 58 | case 13: 59 | if (this->idc2sensor != nullptr) { 60 | uint16_t v = this->messagedata[30] + this->messagedata[31]*256; 61 | this->idc2sensor->publish_state((float)v * 0.1f); 62 | } 63 | break; 64 | case 12: 65 | if (this->iacusensor != nullptr) { 66 | uint16_t v = this->messagedata[10] + this->messagedata[11]*256; 67 | this->iacusensor->publish_state((float)v * 0.1f); 68 | } 69 | break; 70 | case 11: 71 | if (this->iacvsensor != nullptr) { 72 | uint16_t v = this->messagedata[76] + this->messagedata[77]*256; 73 | this->iacvsensor->publish_state((float)v * 0.1f); 74 | } 75 | break; 76 | case 10: 77 | if (this->iacwsensor != nullptr) { 78 | uint16_t v = this->messagedata[72] + this->messagedata[73]*256; 79 | this->iacwsensor->publish_state((float)v * 0.1f); 80 | } 81 | break; 82 | case 9: 83 | if (this->pdc1sensor != nullptr) { 84 | uint16_t v1 = this->messagedata[4] + this->messagedata[5]*256; 85 | uint16_t i1 = this->messagedata[6] + this->messagedata[7]*256; 86 | this->pdc1sensor->publish_state((float)v1 * (float)i1 * 0.01f); 87 | } 88 | break; 89 | case 8: 90 | if (this->pdc2sensor != nullptr) { 91 | uint16_t v2 = this->messagedata[28] + this->messagedata[29]*256; 92 | uint16_t i2 = this->messagedata[30] + this->messagedata[31]*256; 93 | this->pdc2sensor->publish_state((float)v2 * (float)i2 * 0.01f); 94 | } 95 | break; 96 | case 7: 97 | if (this->pactotalsensor != nullptr) { 98 | uint16_t v = this->messagedata[59] + this->messagedata[60]*256; 99 | this->pactotalsensor->publish_state((float)v); 100 | } 101 | break; 102 | case 6: 103 | if (this->vaactotalsensor != nullptr) { 104 | uint16_t v = this->messagedata[65] + this->messagedata[66]*256; 105 | this->vaactotalsensor->publish_state((float)v); 106 | } 107 | break; 108 | case 5: 109 | if (this->pfacsensor != nullptr) { 110 | uint16_t v = this->messagedata[68] + this->messagedata[69]*256; 111 | this->pfacsensor->publish_state((float)v * 0.001f); 112 | } 113 | break; 114 | case 4: 115 | if (this->edaysensor != nullptr) { 116 | uint16_t v = this->messagedata[37] + this->messagedata[38]*256; 117 | this->edaysensor->publish_state((float)v*0.1f); 118 | } 119 | break; 120 | case 3: 121 | if (this->emonthsensor != nullptr) { 122 | uint16_t v = this->messagedata[33] + this->messagedata[34]*256; 123 | this->emonthsensor->publish_state((float)v); 124 | } 125 | break; 126 | case 2: 127 | if (this->etotalsensor != nullptr) { 128 | uint16_t v = this->messagedata[14] + this->messagedata[15]*256; 129 | this->etotalsensor->publish_state((float)v); 130 | } 131 | break; 132 | case 1: 133 | if (this->tigbtsensor != nullptr) { 134 | uint16_t v = this->messagedata[12] + this->messagedata[13]*256; 135 | this->tigbtsensor->publish_state((float)v*0.1f); 136 | } 137 | break; 138 | } 139 | this->sensorupdateprogress--; 140 | } 141 | 142 | static char buffer[SOLIS_S5_SERIAL_BUFFER_LEN] = {0}; 143 | static uint8_t index = 0; 144 | static uint8_t loopwait = 0; 145 | 146 | while (available()) { 147 | buffer[index] = read(); 148 | index++; 149 | index%=SOLIS_S5_SERIAL_BUFFER_LEN; 150 | loopwait = 0; 151 | } 152 | if (index > 0) { 153 | loopwait++; 154 | } 155 | 156 | if (loopwait > SOLIS_S5_LOOP_WAIT) { // some time has passed without receiving another character. this should be the end of a message. 157 | ESP_LOGV(TAG, "message recieved len=%d", index); 158 | if (buffer[0] == 126) { // message starts with the right preamble 159 | uint8_t msglen = buffer[3]; 160 | if (index == msglen + 5) { // messasge has correct length 161 | uint8_t csum = 0; 162 | for (uint8_t i = 1; i < msglen+4; i++) { // csum after preamble, before tx csum 163 | csum += buffer[i]; 164 | } 165 | if (csum == buffer[msglen+4]) { // checksum ok 166 | if ((buffer[2] == 161) && (msglen == 80)) { // inverter response 167 | memcpy(this->messagedata,buffer,SOLIS_S5_SERIAL_BUFFER_LEN); // copy message for processing on next update cycle 168 | this->messagelength = index; // length > 0 indicates the message data has been updated / ready for parsing 169 | ESP_LOGD(TAG, "inverter data received"); 170 | } else if ((buffer[2] == 193) && (msglen == 40)) { // inverter config response 171 | ESP_LOGD(TAG, "inverter config response received"); 172 | } 173 | } else { 174 | ESP_LOGV(TAG, "message checksum fail; discarding. csum = 0x%02X, check = 0x%02X", buffer[msglen+4], csum); 175 | } 176 | } else if ((msglen == 0) && (index == 55)) { // wifi stick command 177 | ESP_LOGD(TAG, "wifi stick command received; ignoring"); 178 | } else { 179 | ESP_LOGV(TAG, "message insufficient length (requested: %d, received: %d); discarding", msglen+5, index); 180 | } 181 | } else { 182 | ESP_LOGV(TAG, "message received, invalid start character"); 183 | } 184 | // reset message, ready for next one 185 | loopwait = 0; 186 | index = 0; 187 | } 188 | 189 | } 190 | 191 | void SolisS5Component::update() { 192 | if (this->messagelength > 0) { // if there's a message pending 193 | ESP_LOGV(TAG, "processing latest message"); 194 | this->messagelength = 0; // indicate message is parsed 195 | this->sensorupdateprogress = SOLIS_S5_SENSOR_COUNT; // start to update sensors 196 | } else { 197 | ESP_LOGV(TAG, "no data received"); 198 | } 199 | } 200 | 201 | void SolisS5Component::dump_config() { 202 | ESP_LOGCONFIG(TAG, "Solis S5 Component"); 203 | } 204 | 205 | } // namespace dekacontroller 206 | } // namespace esphome 207 | -------------------------------------------------------------------------------- /components/solis_s5/solis_s5.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | //#include "esphome/components/text_sensor/text_sensor.h" 6 | //#include "esphome/components/binary_sensor/binary_sensor.h" 7 | #include "esphome/components/uart/uart.h" 8 | 9 | #define SOLIS_S5_LOOP_WAIT 3 10 | #define SOLIS_S5_SERIAL_BUFFER_LEN 150 11 | #define SOLIS_S5_SENSOR_COUNT 19 12 | 13 | namespace esphome { 14 | namespace solis_s5 { 15 | 16 | class SolisS5Component : public PollingComponent, public uart::UARTDevice { 17 | public: 18 | 19 | void setup() override; 20 | void loop() override; 21 | void update() override; 22 | void dump_config() override; 23 | 24 | void set_vdc_1_sensor(sensor::Sensor *s) { vdc1sensor = s; } 25 | void set_vdc_2_sensor(sensor::Sensor *s) { vdc2sensor = s; } 26 | void set_vac_u_sensor(sensor::Sensor *s) { vacusensor = s; } 27 | void set_vac_v_sensor(sensor::Sensor *s) { vacvsensor = s; } 28 | void set_vac_w_sensor(sensor::Sensor *s) { vacwsensor = s; } 29 | 30 | void set_idc_1_sensor(sensor::Sensor *s) { idc1sensor = s; } 31 | void set_idc_2_sensor(sensor::Sensor *s) { idc2sensor = s; } 32 | void set_iac_u_sensor(sensor::Sensor *s) { iacusensor = s; } 33 | void set_iac_v_sensor(sensor::Sensor *s) { iacvsensor = s; } 34 | void set_iac_w_sensor(sensor::Sensor *s) { iacwsensor = s; } 35 | 36 | void set_pdc_1_sensor(sensor::Sensor *s) { pdc1sensor = s; } 37 | void set_pdc_2_sensor(sensor::Sensor *s) { pdc2sensor = s; } 38 | void set_pac_total_sensor(sensor::Sensor *s) { pactotalsensor = s; } 39 | void set_vaac_total_sensor(sensor::Sensor *s) { vaactotalsensor = s; } 40 | void set_pfac_sensor(sensor::Sensor *s) { pfacsensor = s; } 41 | 42 | void set_e_day_sensor(sensor::Sensor *s) { edaysensor = s; } 43 | void set_e_month_sensor(sensor::Sensor *s) { emonthsensor = s; } 44 | void set_e_total_sensor(sensor::Sensor *s) { etotalsensor = s; } 45 | 46 | void set_t_igbt_sensor(sensor::Sensor *s) { tigbtsensor = s; } 47 | 48 | protected: 49 | 50 | sensor::Sensor *vdc1sensor; 51 | sensor::Sensor *vdc2sensor; 52 | sensor::Sensor *vacusensor; 53 | sensor::Sensor *vacvsensor; 54 | sensor::Sensor *vacwsensor; 55 | 56 | sensor::Sensor *idc1sensor; 57 | sensor::Sensor *idc2sensor; 58 | sensor::Sensor *iacusensor; 59 | sensor::Sensor *iacvsensor; 60 | sensor::Sensor *iacwsensor; 61 | 62 | sensor::Sensor *pdc1sensor; 63 | sensor::Sensor *pdc2sensor; 64 | sensor::Sensor *pactotalsensor; 65 | sensor::Sensor *vaactotalsensor; 66 | sensor::Sensor *pfacsensor; 67 | 68 | sensor::Sensor *edaysensor; 69 | sensor::Sensor *emonthsensor; 70 | sensor::Sensor *etotalsensor; 71 | 72 | sensor::Sensor *tigbtsensor; 73 | 74 | char messagedata[SOLIS_S5_SERIAL_BUFFER_LEN] = {0}; 75 | uint8_t messagelength = 0; 76 | volatile uint8_t sensorupdateprogress = 0; 77 | 78 | }; 79 | 80 | } // namespace solis_s5 81 | } // namespace esphome 82 | -------------------------------------------------------------------------------- /solis_piggyback_schematic_0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grob6000/esphome-externalcomponents/1ae1794af21cafafdfb06ee3962738312bbf3098/solis_piggyback_schematic_0.pdf -------------------------------------------------------------------------------- /soliss5-32.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: soliss5 3 | 4 | external_components: 5 | # source: 6 | # type: local 7 | # path: ./components 8 | source: 9 | type: git 10 | url: https://github.com/grob6000/esphome-externalcomponents 11 | 12 | esp32: 13 | board: nodemcu-32s 14 | 15 | # Enable logging 16 | logger: 17 | baud_rate: 0 18 | level: debug 19 | 20 | # Enable Home Assistant API 21 | api: 22 | encryption: 23 | key: !secret encryption1 24 | reboot_timeout: 0s 25 | 26 | web_server: 27 | port: 80 28 | 29 | ota: 30 | password: !secret otapassword1 31 | 32 | wifi: 33 | networks: 34 | - ssid: !secret wifi_ssid 35 | password: !secret wifi_password 36 | hidden: true 37 | domain: !secret domain 38 | fast_connect: true 39 | 40 | sensor: 41 | - platform: uptime 42 | name: Solis Inverter ESPHome Uptime 43 | update_interval: 60s 44 | - platform: wifi_signal 45 | name: Solis Inverter ESPHome Wifi Signal 46 | update_interval: 60s 47 | 48 | uart: 49 | id: uart_bus 50 | tx_pin: 1 51 | rx_pin: 3 52 | baud_rate: 9600 53 | #debug: # uncomment this to see the raw data in the log - useful if you need to decode a different protocol 54 | # direction: BOTH 55 | # dummy_receiver: false 56 | # after: 57 | # timeout: 100ms 58 | 59 | solis_s5: 60 | id: solisinverter 61 | uart_id: uart_bus 62 | update_interval: 60s 63 | voltage_dc_1: 64 | name: "Solis Inverter DC Voltage 1" 65 | voltage_dc_2: 66 | name: "Solis Inverter DC Voltage 2" 67 | voltage_ac_u: 68 | name: "Solis Inverter AC Voltage U" 69 | voltage_ac_v: 70 | name: "Solis Inverter AC Voltage V" 71 | voltage_ac_w: 72 | name: "Solis Inverter AC Voltage W" 73 | current_dc_1: 74 | name: "Solis Inverter DC Current 1" 75 | current_dc_2: 76 | name: "Solis Inverter DC Current 2" 77 | current_ac_u: 78 | name: "Solis Inverter AC Current U" 79 | current_ac_v: 80 | name: "Solis Inverter AC Current V" 81 | current_ac_w: 82 | name: "Solis Inverter AC Current W" 83 | power_dc_1: 84 | name: "Solis Inverter DC Power 1" 85 | power_dc_2: 86 | name: "Solis Inverter DC Power 2" 87 | power_ac_total: 88 | name: "Solis Inverter AC Power Total" 89 | va_ac_total: 90 | name: "Solis Inverter AC VA Total" 91 | powerfactor_ac: 92 | name: "Solis Inverter AC Power Factor" 93 | energy_today: 94 | name: "Solis Inverter Energy Today" 95 | energy_thismonth: 96 | name: "Solis Inverter Energy This Month" 97 | energy_total: 98 | name: "Solis Inverter Energy Total" 99 | temperature_igbt: 100 | name: "Solis Inverter IGBT Temperature" 101 | -------------------------------------------------------------------------------- /soliss5.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: soliss5 3 | 4 | external_components: 5 | # source: 6 | # type: local 7 | # path: ./components 8 | source: 9 | type: git 10 | url: https://github.com/grob6000/esphome-externalcomponents 11 | 12 | esp8266: 13 | board: esp12e 14 | 15 | # Enable logging 16 | logger: 17 | baud_rate: 0 18 | level: debug 19 | 20 | # Enable Home Assistant API 21 | api: 22 | encryption: 23 | key: !secret encryption1 24 | reboot_timeout: 0s 25 | 26 | #web_server: 27 | # port: 80 28 | 29 | ota: 30 | password: !secret otapassword1 31 | 32 | wifi: 33 | networks: 34 | - ssid: !secret wifi_ssid 35 | password: !secret wifi_password 36 | hidden: true 37 | domain: !secret domain 38 | fast_connect: true 39 | 40 | sensor: 41 | - platform: uptime 42 | name: Solis Inverter ESPHome Uptime 43 | update_interval: 60s 44 | - platform: wifi_signal 45 | name: Solis Inverter ESPHome Wifi Signal 46 | update_interval: 60s 47 | 48 | uart: 49 | id: uart_bus 50 | tx_pin: 1 51 | rx_pin: 3 52 | baud_rate: 9600 53 | #debug: # uncomment this to see the raw data in the log - useful if you need to decode a different protocol 54 | # direction: BOTH 55 | # dummy_receiver: false 56 | # after: 57 | # timeout: 100ms 58 | 59 | solis_s5: 60 | id: solisinverter 61 | uart_id: uart_bus 62 | update_interval: 60s 63 | voltage_dc_1: 64 | name: "Solis Inverter DC Voltage 1" 65 | voltage_dc_2: 66 | name: "Solis Inverter DC Voltage 2" 67 | voltage_ac_u: 68 | name: "Solis Inverter AC Voltage U" 69 | voltage_ac_v: 70 | name: "Solis Inverter AC Voltage V" 71 | voltage_ac_w: 72 | name: "Solis Inverter AC Voltage W" 73 | current_dc_1: 74 | name: "Solis Inverter DC Current 1" 75 | current_dc_2: 76 | name: "Solis Inverter DC Current 2" 77 | current_ac_u: 78 | name: "Solis Inverter AC Current U" 79 | current_ac_v: 80 | name: "Solis Inverter AC Current V" 81 | current_ac_w: 82 | name: "Solis Inverter AC Current W" 83 | power_dc_1: 84 | name: "Solis Inverter DC Power 1" 85 | power_dc_2: 86 | name: "Solis Inverter DC Power 2" 87 | power_ac_total: 88 | name: "Solis Inverter AC Power Total" 89 | va_ac_total: 90 | name: "Solis Inverter AC VA Total" 91 | powerfactor_ac: 92 | name: "Solis Inverter AC Power Factor" 93 | energy_today: 94 | name: "Solis Inverter Energy Today" 95 | energy_thismonth: 96 | name: "Solis Inverter Energy This Month" 97 | energy_total: 98 | name: "Solis Inverter Energy Total" 99 | temperature_igbt: 100 | name: "Solis Inverter IGBT Temperature" 101 | -------------------------------------------------------------------------------- /tools/soliss5_protocoltest.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import serial 3 | from time import sleep 4 | import sys 5 | 6 | MSGSPLIT = 55 7 | OUTPUTNAME = "out.csv" 8 | PARSEDNAME = "out_parsed.csv" 9 | 10 | def toSigned32(n): 11 | n = n & 0xffffffff 12 | return (n ^ 0x80000000) - 0x80000000 13 | 14 | def parsemsg(msg,file,timestamp): 15 | pm = {"file":file,"timestamp":timestamp} 16 | pm["start"] = msg[0] # 0x7E 17 | pm["address"] = msg[1] # 1 18 | pm["command"] = msg[2] # 0xA1 19 | pm["length"] = msg[3] # 80 20 | if pm["command"] == 161 and pm["length"] == 80: 21 | pm["vdc_1"] = (msg[4] + msg[5]*256)*0.1 #V CONFIRMED 22 | pm["idc_1"] = (msg[6] + msg[7]*256)*0.1 #A CONFIRMED 23 | pm["vac_1"] = (msg[8] + msg[9]*256)*0.1 #V(RMS) CONFIRMED 24 | pm["iac_1"] = (msg[10] + msg[11])*0.1 #A(RMS) CONFIRMED 25 | pm["igbt_temp"] = (msg[12] + msg[13]*256)*0.1 #degC CONFIRMED 26 | pm["e_total"] = (msg[14] + msg[15]*256)*1.0 #kWh CONFIRMED 27 | # 16 = 0 28 | # 17 = 0 29 | # 18 = 3 30 | # 19 = 0 31 | # 20 = 0 32 | # 21 = 0 33 | # 22 = 19 \ =13075 34 | # 23 = 51 / 35 | pm["freq"] = (msg[24] + msg[25]*256)*0.01 #Hz CONFIRMED 36 | # 26 = 105 \ =1129 37 | # 27 = 4 / 38 | pm["vdc_2"] = (msg[28] + msg[29]*256)*0.1 #V CONFIRMED 39 | pm["idc_2"] = (msg[30] + msg[31]*256)*0.1 #A CONFIRMED 40 | # 32 = 190 41 | pm["e_thismonth"] = (msg[33] + msg[34]*256)*1.0 #kWh CONFIRMED 42 | # 35 = 0 43 | # 36 = 0 44 | pm["e_today"] = (msg[37] + msg[38]*256)*0.1 #kW CONFIRMED 45 | pm["e_yesterday"] = (msg[39] + msg[40]*256)*0.1 #kWh 46 | pm["e_lastmonth"] = (msg[41] + msg[42]*256)*1.0 #kWh 47 | # 43 = 19 48 | # 44 = 2 49 | # 45 = 24 50 | # 46 = 22 51 | # 47 = 0 52 | # 48 = 132 53 | # 49 = 0 54 | # 50 = 0 55 | # 51 = 0 56 | # 52 = 0 57 | # 53 = 0 58 | # 54 = 0 59 | # 55 = 0 60 | # 56 = 0 61 | # 57 = 248 \ = 11000 62 | # 58 = 42 / 63 | pm["p_output"] = (msg[59] + msg[60]*256)*1.0 #W CONFIRMED 64 | # 61=0 65 | pm["unknown_62"] = msg[62] 66 | pm["unknown_63"] = msg[63] 67 | pm["unknown_64"] = msg[64] 68 | pm["va_output"] = (msg[65] + msg[66]*256)*1.0 #VA CONFIRMED 69 | # 67=0 70 | pm["pf"] = (msg[68] + msg[69]*256)*0.001 # 1/1 CONFIRMED 71 | pm["vac_3"] = (msg[70] + msg[71]*256)*0.1 #V CONFIRMED 72 | pm["iac_3"] = (msg[72] + msg[73]*256)*0.1 #A CONFIRMED 73 | pm["vac_2"] = (msg[74] + msg[75]*256)*0.1 #V CONFIRMED 74 | pm["iac_2"] = (msg[76] + msg[77]*256)*0.1 #A CONFIRMED 75 | #78 = 0 76 | #79 = 0 77 | #80 = 0 78 | #81 = 2 79 | #82 = 0 80 | pm["unknown_83"] = msg[83] # 115 81 | pm["crc"] = msg[84] # CONFIRMED 82 | pm["crc_check"] = 0 83 | for i in range(1,len(msg)-1): 84 | pm["crc_check"] += msg[i] 85 | pm["crc_check"]%=256 86 | print("crc={0},crc_check={1}".format(pm["crc"],pm["crc_check"])) 87 | elif (pm["command"] == 193 and pm["length"] == 40): 88 | #4 = 4 89 | #5 = 5 90 | #20-35 = '192.168.1.202' (null terminated) 91 | pass 92 | 93 | return pm 94 | 95 | def putword(word, msg, index): 96 | word = int(word) % (256*256) 97 | msg[index] = word%256 98 | msg[index+1] = int(word/256) 99 | 100 | def hexprint(msg): 101 | s = "MSG: " 102 | for b in msg: 103 | s += "{0:02X} ".format(b) 104 | print(s) 105 | 106 | def sendmessage(ser=None): 107 | msg = [] 108 | for i in range(0,85): 109 | msg.append(0) 110 | msg[0] = 0x7E 111 | msg[1] = 0x01 112 | msg[2] = 0xA1 113 | msg[3] = 80 114 | putword(2001,msg,4) #vdc1 115 | putword(10,msg,6) # idc1 116 | putword(2002,msg,28) # vdc2 117 | putword(11,msg,30) # idc2 118 | putword(2501,msg,8) #vacu 119 | putword(61,msg,10) #iacu 120 | putword(2502,msg,74) #vacv 121 | putword(62,msg,76) #iacv 122 | putword(2503,msg,70) #vacw 123 | putword(63,msg,72) #iacw 124 | putword(401,msg,12) #tigbt 125 | putword(1001,msg,14) #e_total 126 | putword(5000,msg,24) #freq 127 | putword(101,msg,33) #e_month 128 | putword(301,msg,37) #e_today 129 | putword(302,msg,39) #e_yesterday 130 | putword(102,msg,41) #e_lastmonth 131 | va = 250.1*6.1+250.2*6.2+250.3*5.3 132 | putword(int(va*.95),msg,59) # outputpower 133 | putword(950,msg,68) # pf 134 | putword(int(va),msg,65) # va 135 | csum = 0 136 | for i in range(1,84): 137 | csum += msg[i] 138 | csum %= 256 139 | msg[84] = csum 140 | #hexprint(msg) 141 | #print(len(bytes(msg))) 142 | closeport = False 143 | if not ser: 144 | ser = serial.Serial("COM7", 9600) 145 | closeport = True 146 | bs = bytes(msg) 147 | print(bs) 148 | print(len(bs)) 149 | ser.write(bs) 150 | ser.flush() 151 | sleep(2) 152 | if closeport: 153 | ser.close() 154 | 155 | 156 | if __name__ == "__main__": 157 | port = "COM4" 158 | mode = "dummyinverter" 159 | 160 | for v in sys.argv: 161 | v = v.lower() 162 | if v.startswith("port="): 163 | port = v.split("=")[1].upper() 164 | elif v.startswith("mode="): 165 | mode = v.split("=")[1] 166 | 167 | if mode == "dummyinverter": 168 | print("solis message sender") 169 | ser = None 170 | try: 171 | ser = serial.Serial(port, 9600) 172 | except Exception as e: 173 | print(e) 174 | print("invalid serial port, quitting.") 175 | 176 | if ser: 177 | try: 178 | while True: 179 | sendmessage(ser=ser) 180 | sleep(5) 181 | except KeyboardInterrupt as e: 182 | quit() 183 | 184 | elif mode == "processoutput": 185 | 186 | print("solis esphome debug output processor") 187 | textfilelist = glob.glob("*.txt") 188 | messages = [] 189 | with open(OUTPUTNAME, "w") as outfile: 190 | outfile.write("file,timestamp,") 191 | for i in range(0,86): 192 | outfile.write(str(i) + ",") 193 | outfile.write("\n") 194 | msglist = [] 195 | for t in textfilelist: 196 | print("reading file: " + t) 197 | with open(t,"r") as f: 198 | lines = f.readlines() 199 | for l in lines: 200 | if "<<<" in l: 201 | hexbytes = l.split("<<<")[1].strip().split(":") 202 | bytes = [ int(h, 16) for h in hexbytes ] 203 | #print(len(bytes)) 204 | #processmsg(bytes) 205 | if len(bytes) == 140: 206 | bytes1 = bytes[:MSGSPLIT] 207 | outfile.write(t + ",") 208 | outfile.write(l[1:9] + ",") # timestamp 209 | for b in bytes1: 210 | outfile.write(str(b) + ",") 211 | outfile.write("\n") 212 | msglist.append(parsemsg(bytes1, t, l[1:9])) 213 | bytes2 = bytes[MSGSPLIT:] 214 | outfile.write(t + ",") 215 | outfile.write(l[1:9] + ",") # timestamp 216 | for b in bytes2: 217 | outfile.write(str(b) + ",") 218 | outfile.write("\n") 219 | msglist.append(parsemsg(bytes2, t, l[1:9])) 220 | else: 221 | outfile.write(t + ",") 222 | outfile.write(l[1:9] + ",") # timestamp 223 | for b in bytes: 224 | outfile.write(str(b) + ",") 225 | outfile.write("\n") 226 | msglist.append(parsemsg(bytes, t, l[1:9])) 227 | with open(PARSEDNAME, "w") as outfile2: 228 | fullkeylist = [] 229 | for msg in msglist: 230 | for k in msg.keys(): 231 | if not k in fullkeylist: 232 | fullkeylist.append(k) 233 | print(fullkeylist) 234 | for k in fullkeylist: 235 | outfile2.write(k + ",") 236 | outfile2.write("\n") 237 | for msg in msglist: 238 | for k in fullkeylist: 239 | if k in msg: 240 | outfile2.write(str(msg[k])) 241 | outfile2.write(",") 242 | outfile2.write("\n") 243 | else: 244 | print("invalid mode, no action") 245 | 246 | -------------------------------------------------------------------------------- /watermeter.yaml: -------------------------------------------------------------------------------- 1 | # usage example of beam_binary_sensor for a water meter 2 | # beam binary sensor is similar to a gpio_binary_sensor 3 | # beam binary sensor will alternate an output (e.g. ir led) on and off, and only publish 'on' if during the cycle both: 4 | # 1. sensor is activated when drive is on 5 | # 2. sensor is deactivated when drive is off 6 | # this ensures that the sensor is not activated erroneously by ambient light (e.g. sunlight) 7 | # may be useful for sensors such as electricity meters (where a red led is used, and might be badly affected by ambient light), outdoor ir beams or reflective sensors, etc. 8 | 9 | esphome: 10 | name: watermeter_example 11 | 12 | esp8266: 13 | board: esp01_1m 14 | 15 | # load the custom external components from my repo 16 | external_components: 17 | source: 18 | type: git 19 | url: https://github.com/grob6000/esphome-externalcomponents 20 | 21 | # Enable logging 22 | logger: 23 | #baud_rate: 0 24 | level: DEBUG 25 | 26 | # Enable Home Assistant API with encryption 27 | api: 28 | encryption: 29 | key: !secret encryption1 30 | 31 | # over-the-air connection password; secret 32 | ota: 33 | password: !secret ota_password1 34 | 35 | # wifi details - more secrets! 36 | wifi: 37 | networks: 38 | - ssid: !secret wifi_ssid 39 | password: !secret wifi_password 40 | hidden: true 41 | domain: !secret domain 42 | fast_connect: true 43 | 44 | # global storage for the pulse count 45 | globals: 46 | - id: pulsecount 47 | type: int 48 | restore_value: no 49 | initial_value: '0' 50 | 51 | # water meter sensor for use by home assistant 52 | # for this water meter, the factor (1.0f) is L/pulse; adjust as needed 53 | # in home assistant, you can set up 'utility_meter' entities with this as the source; e.g. 54 | # 55 | # utility_meter: 56 | # utility_meter_water: 57 | # name: Water Utility Meter 58 | # unique_id: utility_meter_water 59 | # source: sensor.water_meter_total 60 | # cycle: daily 61 | sensor: 62 | - platform: template 63 | id: watermeter 64 | name: Water Meter Total 65 | update_interval: 60s 66 | lambda: |- 67 | return (float)id(pulsecount) * 1.0f; 68 | unit_of_measurement: L 69 | state_class: total_increasing 70 | 71 | # beam binary sensor example 72 | # for use by esphome only (thus internal=true - set this to false if you want it in the UI) 73 | # TROUBLESHOOTING TIP: if you get one of the sensor or driver pins inverted, the sensor will always be off. if you invert both, it will... work fine (this is symmetric). 74 | beam_binary_sensor: 75 | id: pulsesensor 76 | name: Water Meter Pulse Sensor 77 | internal: true 78 | on_click: 79 | min_length: 10ms 80 | then: 81 | - lambda: |- 82 | id(pulsecount) += 1; 83 | # sensor pin: connect to the phototransistor output. in my case, this is active low, so inverted=true 84 | pin_sensor: 85 | number: 14 86 | inverted: true 87 | # driver pin: connect to the ir led (via resistor). in my case, this is active high, so inverted=false (default) 88 | pin_driver: 89 | number: 12 90 | # output pin: this pin mirrors the raw sensor state. in my case this is esp12 led, which is active low, so inverted=true 91 | pin_output: 92 | number: 2 93 | inverted: true 94 | 95 | --------------------------------------------------------------------------------