├── .gitignore ├── LICENSE ├── README.md ├── components └── p1_mini │ ├── __init__.py │ ├── p1_mini.cpp │ ├── p1_mini.h │ ├── sensor │ ├── __init__.py │ ├── p1_mini_sensor.cpp │ └── p1_mini_sensor.h │ └── text_sensor │ ├── __init__.py │ ├── p1_mini_text_sensor.cpp │ └── p1_mini_text_sensor.h ├── docs ├── NO-RTS.md ├── build_c3_zero.md ├── build_d1_mini.md ├── component_only.md ├── passthrough.md └── troubleshooting.md ├── images ├── C3-complete.jpg ├── C3-pullup.jpg ├── C3-wiring.jpg ├── D1mini-pins.png ├── RJ12-pins.png ├── completed.png ├── header.jpg ├── inHA.png ├── secondary_experimental.png ├── secondary_pins.png ├── signal-bad.jpg ├── signal-good.jpg └── soldered.png ├── p1mini.yaml └── p1mini32.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | /secrets.yaml 2 | /.esphome 3 | electricity_meter 4 | /*.sh 5 | .vs 6 | __pycache__ 7 | *.bak 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Johnny Johansson 4 | Copyright (c) 2022 Erik Björk 5 | Copyright (c) 2020 Pär Svanström 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Header image](images/header.jpg) 2 | 3 | # esphome-p1mini 4 | Based on esphome-p1reader, which is an ESPHome custom component for reading P1 data from electricity meters. Designed for Swedish meters that implements the specification defined in the [Swedish Energy Industry Recommendation For Customer Interfaces](https://www.energiforetagen.se/forlag/elnat/branschrekommendation-for-lokalt-kundgranssnitt-for-elmatare/) version 1.3 and above. 5 | 6 | The component can be used [by itself from any config file](docs/component_only.md) or with one of the config files included in the project, which matches the suggested hardware configurations and is kept up to date with any updates to the component. 7 | 8 | Notable differences from esphome-p1reader are: 9 | * More frequent update of sensors with configurable update period. 10 | * No additional components needed. RJ12 cable connects directly to ESP module. (A resistor may be needed in some cases) 11 | * Code rewritten to not spend excessive amounts of time in calls to the `loop` function. This should ensure stable operation of ESPHome and might help prevent some serial communication issues. 12 | 13 | ## ESPHome version 14 | The current version is tested with ESPHome version `2025.5.0` and the yaml *will not work with versions earlier than `2024.6.0`*. 15 | 16 | ## Verified meter hardware / supplier 17 | * [Sagemcom T211](https://www.ellevio.se/globalassets/content/el/elmatare-produktblad-b2c/ellevio_produktblad_fas3_t211_web2.pdf) / Ellevio, Skånska Energi 18 | * [Aidon 6534](https://jonkopingenergi.se/storage/B9A468B538E9CF48DF5E276BDA7D2D12727D152110286963E9D603D67B849242/5009da534dbc44b6a34cb0bed31cfd5c/pdf/media/b53a4057862646cbb22702a847a291a2/Aidon%206534%20bruksansvisning.pdf) with RJ12/P1-port module (*not* RJ45/NVE module) / SEVAB 19 | * [Landis+Gyr E360](https://eu.landisgyr.com/blog-se/e360-en-smart-matare-som-optimerarden-totala-agandekostnaden) / E.ON - [But read this](docs/NO-RTS.md#landisgyr-e360) 20 | * [S34U18 (Sanxing SX631)](https://www.vattenfalleldistribution.se/matarbyte/nya-elmataren/) / Vattenfall - [But read this](docs/NO-RTS.md#s34u18-sanxing-sx631) 21 | * Kamstrup OMNIPOWER 22 | * [KAIFA MA304H4E](https://reko.nackaenergi.se/elmatarbyte/) (and MA304T4E) / Nacka Energi - [But read this](docs/NO-RTS.md#kaifa-ma304t4e--ma304h4e) 23 | * [SWEMET / Shenzhen Star - STZ351](https://www.veab.se/globalassets/dokumentarkiv/manualer-och-skotselrad/anvandarmanual-elmatare-3-fas.pdf) - Some meters are working fine while other seems to have an incorrectly formatted message and incorrectly calculated checksum. *If* you are having problems, look at [this discussion](https://github.com/Beaky2000/esphome-p1mini/issues/26) for a possible workaround. 24 | 25 | ## Meters verified with esphome-p1reader, which should work too... 26 | * [Itron A300](https://boraselnat.se/elnat/elmatarbyte-2020-2021/sa-har-fungerar-din-nya-elmatare/) / Borås Elnät 27 | * [KAIFA CL109](https://www.oresundskraft.se/dags-for-matarbyte/) / Öresundskraft 28 | 29 | ## Hardware 30 | ### Wemos D1 Mini 31 | This project is named after the Wemos D1 mini board, which is based on the ESP8266 processor. D1 mini boards (or clones) are very cheap and still work well. 32 | 33 | [The build instructions for the D1 mini](docs/build_d1_mini.md) match the `p1mini.yaml` configuration. 34 | 35 | ### Waveshare ESP32-C3-Zero 36 | However, the ESP8266 is now over 10 years old and [no longer recommended](https://esphome.io/guides/faq.html) for ESPHome projects. As a result I have moved to using a Waveshare ESP32-C3-Zero board, with a more powerfull processor that does not require more power than the ESP8266. 37 | 38 | [The build instructions for the C3-Zero](docs/build_c3_zero.md) match the `p1mini32.yaml` configuration. 39 | 40 | ### ... or anything else 41 | It is also fairly easy to take any board that ESPHome supports and modifying one of the configurations to work with that. It is mostly a question of figuring out what pins to use for what. If you have pre built hardware which does not connect the RTS signal to a GPIO, [read this](docs/NO-RTS.md#rts-not-attached-to-a-gpio). Also, if your pre built hardware inverts the signal in hardware, make sure to remove the inversion in the configration! 42 | 43 | Note that ESP32 based boards other than the ESP32-C3 draw more power, which may cause a problem with the supply from the meter and generally offer no advantage. The P1 port on the meter provides 5V up to 250mA. 44 | 45 | ## P1 Passthrough 46 | [It is possible to attach another P1 reading device in case you need to connect a car charger (or a second p1-mini...) etc.](docs/passthrough.md). 47 | 48 | ## Installation 49 | The component can be used by itself from any config file, or with one of the included config files, which are kept up to date with any updates and matches one of the hardware configurations. 50 | 51 | ### Standalone 52 | If you are making substantial changes to the config it may make more sense to [use the component only](docs/component_only.md) in your config file. 53 | 54 | ### With one of the included yaml files 55 | Clone the repository and create a companion `secrets.yaml` file with the following fields: 56 | ``` 57 | wifi_ssid: 58 | wifi_password: 59 | p1mini_password: 60 | p1mini_api_key: 61 | ``` 62 | The `p1mini_password` field can be set to any password before doing the initial upload of the firmware. A new API key can be generated on [this page](https://esphome.io/components/api.html). 63 | 64 | The file structure should include these files: 65 | 66 | ``` 67 | |- p1mini.yaml (or p1mini32.yaml) 68 | |- secrets.yaml 69 | |- components 70 | |- p1_mini 71 | |- __init__.py 72 | |- p1_mini.cpp 73 | |- p1_mini.h 74 | |- sensor 75 | |- __init__.py 76 | |- p1_mini_sensor.cpp 77 | |- p1_mini_sensor.h 78 | |- text_sensor 79 | |- __init__.py 80 | |- p1_mini_text_sensor.cpp 81 | |- p1_mini_text_sensor.h 82 | ``` 83 | 84 | Flash ESPHome as usual, with the relevant files in place. *Don't* connect USB and the P1 port at the same time! 85 | 86 | If everything works, Home Assistant will autodetect the new integration after you plug it into the P1 port: 87 | 88 | ![In Home Assistant](images/inHA.png) 89 | 90 | ## Troubleshooting 91 | [Things to try if you are having problems](docs/troubleshooting.md). (Ideally before opening a GitHub Issue) 92 | -------------------------------------------------------------------------------- /components/p1_mini/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import uart 4 | from esphome.const import CONF_ID, CONF_TRIGGER_ID 5 | from esphome import automation 6 | 7 | DEPENDENCIES = ['uart'] 8 | p1_mini_ns = cg.esphome_ns.namespace('p1_mini') 9 | P1Mini = p1_mini_ns.class_('P1Mini', cg.Component, uart.UARTDevice) 10 | MULTI_CONF = True 11 | 12 | CONF_P1_MINI_ID = "p1_mini_id" 13 | CONF_OBIS_CODE = "obis_code" 14 | CONF_IDENTIFIER = "identifier" 15 | CONF_MINIMUM_PERIOD = "minimum_period" 16 | CONF_BUFFER_SIZE = "buffer_size" 17 | CONF_SECONDARY_P1 = "secondary_p1" 18 | CONF_ON_READY_TO_RECEIVE = "on_ready_to_receive" 19 | CONF_ON_RECEIVING_UPDATE = "on_receiving_update" 20 | CONF_ON_UPDATE_RECEIVED = "on_update_received" 21 | CONF_ON_UPDATE_PROCESSED = "on_update_processed" 22 | CONF_ON_COMMUNICATION_ERROR = "on_communication_error" 23 | 24 | 25 | 26 | # Triggers 27 | ReadyToReceiveTrigger = p1_mini_ns.class_("ReadyToReceiveTrigger", automation.Trigger.template()) 28 | ReceivingUpdateTrigger = p1_mini_ns.class_("ReceivingUpdateTrigger", automation.Trigger.template()) 29 | UpdateReceivedTrigger = p1_mini_ns.class_("UpdateReceivedTrigger", automation.Trigger.template()) 30 | UpdateProcessedTrigger = p1_mini_ns.class_("UpdateProcessedTrigger", automation.Trigger.template()) 31 | CommunicationErrorTrigger = p1_mini_ns.class_("CommunicationErrorTrigger", automation.Trigger.template()) 32 | 33 | CONFIG_SCHEMA = cv.Schema({ 34 | cv.GenerateID(): cv.declare_id(P1Mini), 35 | cv.Optional(CONF_SECONDARY_P1, False): cv.boolean, 36 | cv.Optional(CONF_MINIMUM_PERIOD, default="0s"): cv.time_period, 37 | cv.Optional(CONF_BUFFER_SIZE, default=3072): cv.int_range(min=512, max=32768), 38 | cv.Optional(CONF_ON_READY_TO_RECEIVE): automation.validate_automation( 39 | { 40 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyToReceiveTrigger), 41 | } 42 | ), 43 | cv.Optional(CONF_ON_RECEIVING_UPDATE): automation.validate_automation( 44 | { 45 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReceivingUpdateTrigger), 46 | } 47 | ), 48 | cv.Optional(CONF_ON_UPDATE_RECEIVED): automation.validate_automation( 49 | { 50 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UpdateReceivedTrigger), 51 | } 52 | ), 53 | cv.Optional(CONF_ON_UPDATE_PROCESSED): automation.validate_automation( 54 | { 55 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UpdateProcessedTrigger), 56 | } 57 | ), 58 | cv.Optional(CONF_ON_COMMUNICATION_ERROR): automation.validate_automation( 59 | { 60 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CommunicationErrorTrigger), 61 | } 62 | ) 63 | }).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) 64 | 65 | async def to_code(config): 66 | var = cg.new_Pvariable( 67 | config[CONF_ID], 68 | config[CONF_MINIMUM_PERIOD].total_milliseconds, 69 | config[CONF_BUFFER_SIZE], 70 | config[CONF_SECONDARY_P1], 71 | ) 72 | await cg.register_component(var, config) 73 | await uart.register_uart_device(var, config) 74 | 75 | for conf in config.get(CONF_ON_READY_TO_RECEIVE, []): 76 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 77 | cg.add(var.register_ready_to_receive_trigger(trigger)) 78 | await automation.build_automation(trigger, [], conf) 79 | 80 | for conf in config.get(CONF_ON_RECEIVING_UPDATE, []): 81 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 82 | cg.add(var.register_receiving_update_trigger(trigger)) 83 | await automation.build_automation(trigger, [], conf) 84 | 85 | for conf in config.get(CONF_ON_UPDATE_RECEIVED, []): 86 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 87 | cg.add(var.register_update_received_trigger(trigger)) 88 | await automation.build_automation(trigger, [], conf) 89 | 90 | for conf in config.get(CONF_ON_UPDATE_PROCESSED, []): 91 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 92 | cg.add(var.register_update_processed_trigger(trigger)) 93 | await automation.build_automation(trigger, [], conf) 94 | 95 | for conf in config.get(CONF_ON_COMMUNICATION_ERROR, []): 96 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 97 | cg.add(var.register_communication_error_trigger(trigger)) 98 | await automation.build_automation(trigger, [], conf) 99 | 100 | def obis_code(value): 101 | value = cv.string(value) 102 | #match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) 103 | # if match is None: 104 | # raise cv.Invalid(f"{value} is not a valid OBIS code") 105 | return value 106 | 107 | def identifier(value): 108 | value = cv.string(value) 109 | return value 110 | -------------------------------------------------------------------------------- /components/p1_mini/p1_mini.cpp: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------------- 2 | // ESPHome P1 Electricity Meter custom sensor 3 | // Copyright 2025 Johnny Johansson 4 | // Copyright 2022 Erik Björk 5 | // Copyright 2020 Pär Svanström 6 | // 7 | // History 8 | // 0.1.0 2020-11-05: Initial release 9 | // 0.2.0 2022-04-13: Major rewrite 10 | // 0.3.0 2022-04-23: Passthrough to secondary P1 device 11 | // 0.4.0 2022-09-20: Support binary format 12 | // 0.5.0 ????-??-??: Rewritten as an ESPHome "external component" 13 | // 0.6.0 2025-01-04: Introduced text sensors 14 | // 15 | // MIT License 16 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 17 | // to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | // and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 25 | // IN THE SOFTWARE. 26 | //------------------------------------------------------------------------------------- 27 | 28 | #include "esphome/core/log.h" 29 | #include "p1_mini.h" 30 | 31 | namespace esphome { 32 | namespace p1_mini { 33 | 34 | namespace { 35 | // Combine the three values defining a sensor into a single unsigned int for easier 36 | // handling and comparison 37 | inline uint32_t OBIS(uint32_t major, uint32_t minor, uint32_t micro) 38 | { 39 | return (major & 0xfff) << 16 | (minor & 0xff) << 8 | (micro & 0xff); 40 | } 41 | 42 | constexpr static uint32_t OBIS_ERROR{ 0xffffffff }; 43 | 44 | inline uint32_t OBIS(char const *code) 45 | { 46 | uint32_t major{ 0 }; 47 | uint32_t minor{ 0 }; 48 | uint32_t micro{ 0 }; 49 | 50 | char const *C{ code }; 51 | while (std::isdigit(*C)) major = major * 10 + (*C++ - '0'); 52 | if (*C++ == '\0') return OBIS_ERROR; 53 | while (std::isdigit(*C)) minor = minor * 10 + (*C++ - '0'); 54 | if (*C++ == '\0') return OBIS_ERROR; 55 | while (std::isdigit(*C)) micro = micro * 10 + (*C++ - '0'); 56 | if (*C++ != '\0') return OBIS_ERROR; 57 | return OBIS(major, minor, micro); 58 | } 59 | 60 | uint16_t crc16_ccitt_false(char *pData, int length) { 61 | int i; 62 | uint16_t wCrc = 0; 63 | while (length--) { 64 | wCrc ^= *(unsigned char *)pData++; 65 | for (i = 0; i < 8; i++) 66 | wCrc = wCrc & 0x0001 ? (wCrc >> 1) ^ 0xA001 : wCrc >> 1; 67 | } 68 | return wCrc; 69 | } 70 | 71 | uint16_t crc16_x25(char *pData, int length) { 72 | int i; 73 | uint16_t wCrc = 0xffff; 74 | while (length--) { 75 | wCrc ^= *(unsigned char *)pData++ << 0; 76 | for (i = 0; i < 8; i++) 77 | wCrc = wCrc & 0x0001 ? (wCrc >> 1) ^ 0x8408 : wCrc >> 1; 78 | } 79 | return wCrc ^ 0xffff; 80 | } 81 | 82 | constexpr static const char *TAG = "P1Mini"; 83 | 84 | 85 | 86 | } 87 | 88 | 89 | P1MiniSensorBase::P1MiniSensorBase(std::string obis_code) 90 | : m_obis{ OBIS(obis_code.c_str()) } 91 | { 92 | if (m_obis == OBIS_ERROR) ESP_LOGE(TAG, "Not a valid OBIS code: '%s'", obis_code.c_str()); 93 | } 94 | 95 | P1MiniTextSensorBase::P1MiniTextSensorBase(std::string identifier) 96 | : m_identifier{ identifier } 97 | { 98 | //ESP_LOGI(TAG, "New text sensor: '%s'", identifier.c_str()); 99 | } 100 | 101 | P1Mini::P1Mini(uint32_t min_period_ms, int buffer_size, bool secondary_p1) 102 | : m_error_recovery_time{ millis() } 103 | , m_message_buffer_size{ buffer_size } 104 | , m_min_period_ms{ min_period_ms } 105 | , m_secondary_p1{ secondary_p1 } 106 | { 107 | m_message_buffer = new char[m_message_buffer_size]; 108 | if (m_message_buffer == nullptr) { 109 | ESP_LOGE(TAG, "Failed to allocate %d bytes for buffer.", m_message_buffer_size); 110 | static char dummy[2]; 111 | m_message_buffer = dummy; 112 | m_message_buffer_size = 2; 113 | } 114 | else { 115 | m_message_buffer_UP.reset(m_message_buffer); 116 | } 117 | } 118 | 119 | void P1Mini::setup() { 120 | //ESP_LOGD("P1Mini", "setup()"); 121 | } 122 | 123 | void P1Mini::loop() { 124 | unsigned long const loop_start_time{ millis() }; 125 | switch (m_state) { 126 | case states::IDENTIFYING_MESSAGE: 127 | if (!available()) { 128 | constexpr unsigned long max_wait_time_ms{ 60000 }; 129 | if (max_wait_time_ms < loop_start_time - m_identifying_message_time) { 130 | ESP_LOGW(TAG, "No data received for %d seconds.", max_wait_time_ms / 1000); 131 | ChangeState(states::ERROR_RECOVERY); 132 | } 133 | break; 134 | } 135 | { 136 | char const read_byte{ GetByte() }; 137 | if (read_byte == '/') { 138 | ESP_LOGD(TAG, "ASCII data format"); 139 | m_data_format = data_formats::ASCII; 140 | } 141 | else if (read_byte == 0x7e) { 142 | ESP_LOGD(TAG, "BINARY data format"); 143 | m_data_format = data_formats::BINARY; 144 | } 145 | else { 146 | ESP_LOGW(TAG, "Unknown data format (0x%02x). Resetting.", read_byte); 147 | ChangeState(states::ERROR_RECOVERY); 148 | return; 149 | } 150 | m_message_buffer[m_message_buffer_position++] = read_byte; 151 | ChangeState(states::READING_MESSAGE); 152 | } 153 | // Not breaking here! The delay caused by exiting the loop function here can cause 154 | // the UART buffer to overflow, so instead, go directly into the READING_MESSAGE 155 | // part. 156 | case states::READING_MESSAGE: 157 | ++m_num_message_loops; 158 | while (available()) { 159 | // While data is available, read it one byte at a time. 160 | char const read_byte{ GetByte() }; 161 | 162 | m_message_buffer[m_message_buffer_position++] = read_byte; 163 | 164 | // Find out where CRC will be positioned 165 | if (m_data_format == data_formats::ASCII && read_byte == '!') { 166 | // The exclamation mark indicates that the main message is complete 167 | // and the CRC will come next. 168 | m_crc_position = m_message_buffer_position; 169 | } 170 | else if (m_data_format == data_formats::BINARY && m_message_buffer_position == 3) { 171 | if ((0xe0 & m_message_buffer[1]) != 0xa0) { 172 | ESP_LOGW(TAG, "Unknown frame format (0x%02X). Resetting.", read_byte); 173 | ChangeState(states::ERROR_RECOVERY); 174 | return; 175 | } 176 | m_crc_position = ((0x1f & m_message_buffer[1]) << 8) + m_message_buffer[2] - 1; 177 | } 178 | 179 | // If end of CRC is reached, start verifying CRC 180 | if (m_crc_position > 0 && m_message_buffer_position > m_crc_position) { 181 | if (m_data_format == data_formats::ASCII && read_byte == '\n') { 182 | ChangeState(states::VERIFYING_CRC); 183 | return; 184 | } 185 | else if (m_data_format == data_formats::BINARY && m_message_buffer_position == m_crc_position + 3) { 186 | if (read_byte != 0x7e) { 187 | ESP_LOGW(TAG, "Unexpected end. Resetting."); 188 | ChangeState(states::ERROR_RECOVERY); 189 | return; 190 | } 191 | ChangeState(states::VERIFYING_CRC); 192 | return; 193 | } 194 | } 195 | if (m_message_buffer_position == m_message_buffer_size) { 196 | ESP_LOGW(TAG, "Message buffer overrun. Resetting."); 197 | ChangeState(states::ERROR_RECOVERY); 198 | return; 199 | } 200 | 201 | } 202 | { 203 | constexpr unsigned long max_message_time_ms{ 10000 }; 204 | if (max_message_time_ms < loop_start_time - m_reading_message_time && m_reading_message_time < loop_start_time) { 205 | ESP_LOGW(TAG, "Complete message not received within %d seconds. Resetting.", max_message_time_ms / 1000); 206 | ChangeState(states::ERROR_RECOVERY); 207 | } 208 | } 209 | break; 210 | case states::VERIFYING_CRC: { 211 | int crc_from_msg = -1; 212 | int crc = 0; 213 | 214 | if (m_data_format == data_formats::ASCII) { 215 | crc_from_msg = (int)strtol(m_message_buffer + m_crc_position, NULL, 16); 216 | crc = crc16_ccitt_false(m_message_buffer, m_crc_position); 217 | } 218 | else if (m_data_format == data_formats::BINARY) { 219 | crc_from_msg = (m_message_buffer[m_crc_position + 1] << 8) + m_message_buffer[m_crc_position]; 220 | crc = crc16_x25(&m_message_buffer[1], m_crc_position - 1); 221 | } 222 | 223 | if (crc == crc_from_msg) { 224 | ESP_LOGD(TAG, "CRC verification OK"); 225 | if (m_data_format == data_formats::ASCII) { 226 | ChangeState(states::PROCESSING_ASCII); 227 | } 228 | else if (m_data_format == data_formats::BINARY) { 229 | ChangeState(states::PROCESSING_BINARY); 230 | } 231 | else { 232 | ChangeState(states::ERROR_RECOVERY); 233 | } 234 | return; 235 | } 236 | 237 | // CRC verification failed 238 | ESP_LOGW(TAG, "CRC mismatch, calculated %04X != %04X. Message ignored.", crc, crc_from_msg); 239 | if (m_data_format == data_formats::ASCII) { 240 | ESP_LOGD(TAG, "Buffer:\n%s (%d)", m_message_buffer, m_message_buffer_position); 241 | } 242 | else if (m_data_format == data_formats::BINARY) { 243 | ESP_LOGD(TAG, "Buffer:"); 244 | char hex_buffer[81]; 245 | hex_buffer[80] = '\0'; 246 | for (int i = 0; i * 40 < m_message_buffer_position; i++) { 247 | int j; 248 | for (j = 0; j + i * 40 < m_message_buffer_position && j < 40; j++) { 249 | sprintf(&hex_buffer[2 * j], "%02X", m_message_buffer[j + i * 40]); 250 | } 251 | if (j >= m_message_buffer_position) { 252 | hex_buffer[j] = '\0'; 253 | } 254 | ESP_LOGD(TAG, "%s", hex_buffer); 255 | } 256 | } 257 | ChangeState(states::ERROR_RECOVERY); 258 | return; 259 | } 260 | case states::PROCESSING_ASCII: 261 | ++m_num_processing_loops; 262 | do { 263 | while (*m_start_of_data == '\n' || *m_start_of_data == '\r') ++m_start_of_data; 264 | char *end_of_line{ m_start_of_data }; 265 | while (*end_of_line != '\n' && *end_of_line != '\r' && *end_of_line != '\0' && *end_of_line != '!') ++end_of_line; 266 | char const end_of_line_char{ *end_of_line }; 267 | *end_of_line = '\0'; 268 | 269 | if (end_of_line != m_start_of_data) { 270 | int minor{ -1 }, major{ -1 }, micro{ -1 }; 271 | double value{ -1.0 }; 272 | if (sscanf(m_start_of_data, "1-0:%d.%d.%d(%lf", &major, &minor, µ, &value) != 4) { 273 | bool matched_text_sensor{ false }; 274 | for (IP1MiniTextSensor *text_sensor : m_text_sensors) { 275 | if (strncmp(m_start_of_data, text_sensor->Identifier().c_str(), text_sensor->Identifier().size()) == 0) { 276 | matched_text_sensor = true; 277 | text_sensor->publish_val(m_start_of_data); 278 | break; 279 | } 280 | 281 | } 282 | if (!matched_text_sensor) ESP_LOGD(TAG, "No sensor matched line '%s'", m_start_of_data); 283 | } 284 | else { 285 | uint32_t const obisCode{ OBIS(major, minor, micro) }; 286 | auto iter{ m_sensors.find(obisCode) }; 287 | if (iter != m_sensors.end()) iter->second->publish_val(value); 288 | else { 289 | ESP_LOGD(TAG, "No sensor matching: %d.%d.%d (0x%x)", major, minor, micro, obisCode); 290 | } 291 | } 292 | } 293 | *end_of_line = end_of_line_char; 294 | if (end_of_line_char == '\0' || end_of_line_char == '!') { 295 | ChangeState(states::WAITING); 296 | return; 297 | } 298 | m_start_of_data = end_of_line + 1; 299 | } while (millis() - loop_start_time < 25); 300 | break; 301 | case states::PROCESSING_BINARY: { 302 | ++m_num_processing_loops; 303 | if (m_start_of_data == m_message_buffer) { 304 | m_start_of_data += 3; 305 | while (*m_start_of_data != 0x13 && m_start_of_data <= m_message_buffer + m_crc_position) ++m_start_of_data; 306 | if (m_start_of_data > m_message_buffer + m_crc_position) { 307 | ESP_LOGW(TAG, "Could not find control byte. Resetting."); 308 | ChangeState(states::ERROR_RECOVERY); 309 | return; 310 | } 311 | m_start_of_data += 6; 312 | } 313 | 314 | do { 315 | uint8_t type = *m_start_of_data; 316 | switch (type) { 317 | case 0x00: 318 | m_start_of_data++; 319 | break; 320 | case 0x01: // array 321 | m_start_of_data += 2; 322 | break; 323 | case 0x02: // struct 324 | m_start_of_data += 2; 325 | break; 326 | case 0x06: {// unsigned double long 327 | uint32_t v = (*(m_start_of_data + 1) << 24 | *(m_start_of_data + 2) << 16 | *(m_start_of_data + 3) << 8 | *(m_start_of_data + 4)); 328 | float fv = v * 1.0 / 1000; 329 | auto iter{ m_sensors.find(m_obis_code) }; 330 | if (iter != m_sensors.end()) iter->second->publish_val(fv); 331 | m_start_of_data += 1 + 4; 332 | break; 333 | } 334 | case 0x09: // octet 335 | if (*(m_start_of_data + 1) == 0x06) { 336 | int minor{ -1 }, major{ -1 }, micro{ -1 }; 337 | major = *(m_start_of_data + 4); 338 | minor = *(m_start_of_data + 5); 339 | micro = *(m_start_of_data + 6); 340 | 341 | m_obis_code = OBIS(major, minor, micro); 342 | } 343 | m_start_of_data += 2 + (int)*(m_start_of_data + 1); 344 | break; 345 | case 0x0a: // string 346 | m_start_of_data += 2 + (int)*(m_start_of_data + 1); 347 | break; 348 | case 0x0c: // datetime 349 | m_start_of_data += 13; 350 | break; 351 | case 0x0f: // scalar 352 | m_start_of_data += 2; 353 | break; 354 | case 0x10: {// unsigned long 355 | uint16_t v = (*(m_start_of_data + 1) << 8 | *(m_start_of_data + 2)); 356 | float fv = v * 1.0 / 10; 357 | auto iter{ m_sensors.find(m_obis_code) }; 358 | if (iter != m_sensors.end()) iter->second->publish_val(fv); 359 | m_start_of_data += 3; 360 | break; 361 | } 362 | case 0x12: {// signed long 363 | int16_t v = (*(m_start_of_data + 1) << 8 | *(m_start_of_data + 2)); 364 | float fv = v * 1.0 / 10; 365 | auto iter{ m_sensors.find(m_obis_code) }; 366 | if (iter != m_sensors.end()) iter->second->publish_val(fv); 367 | m_start_of_data += 3; 368 | break; 369 | } 370 | case 0x16: // enum 371 | m_start_of_data += 2; 372 | break; 373 | default: 374 | ESP_LOGW(TAG, "Unsupported data type 0x%02x. Resetting.", type); 375 | ChangeState(states::ERROR_RECOVERY); 376 | return; 377 | } 378 | if (m_start_of_data >= m_message_buffer + m_crc_position) { 379 | ChangeState(states::WAITING); 380 | return; 381 | } 382 | } while (millis() - loop_start_time < 25); 383 | break; 384 | } 385 | case states::WAITING: 386 | if (m_display_time_stats) { 387 | m_display_time_stats = false; 388 | if (m_time_stats_as_info_next == ++m_time_stats_counter) { 389 | m_time_stats_as_info_next <<= 1; 390 | ESP_LOGI(TAG, "Cycle times: Identifying = %d ms, Message = %d ms (%d loops), Processing = %d ms (%d loops), (Total = %d ms). %d bytes in buffer", 391 | m_reading_message_time - m_identifying_message_time, 392 | m_processing_time - m_reading_message_time, 393 | m_num_message_loops, 394 | m_waiting_time - m_processing_time, 395 | m_num_processing_loops, 396 | m_waiting_time - m_identifying_message_time, 397 | m_message_buffer_position 398 | ); 399 | } 400 | else 401 | ESP_LOGD(TAG, "Cycle times: Identifying = %d ms, Message = %d ms (%d loops), Processing = %d ms (%d loops), (Total = %d ms). %d bytes in buffer", 402 | m_reading_message_time - m_identifying_message_time, 403 | m_processing_time - m_reading_message_time, 404 | m_num_message_loops, 405 | m_waiting_time - m_processing_time, 406 | m_num_processing_loops, 407 | m_waiting_time - m_identifying_message_time, 408 | m_message_buffer_position 409 | ); 410 | } 411 | if (m_min_period_ms == 0 || m_min_period_ms < loop_start_time - m_identifying_message_time) { 412 | ChangeState(states::IDENTIFYING_MESSAGE); 413 | } 414 | else if (available()) { 415 | ESP_LOGE(TAG, "Data was received before beeing requested. If flow control via the RTS signal is not used, the minimum_period should be set to 0s in the yaml. Resetting."); 416 | ChangeState(states::ERROR_RECOVERY); 417 | } 418 | break; 419 | case states::ERROR_RECOVERY: 420 | if (available()) { 421 | int max_bytes_to_discard{ 200 }; 422 | do { AddByteToDiscardLog(GetByte()); } while (available() && max_bytes_to_discard-- != 0); 423 | } 424 | else if (500 < loop_start_time - m_error_recovery_time) { 425 | ChangeState(states::WAITING); 426 | FlushDiscardLog(); 427 | } 428 | break; 429 | } 430 | } 431 | 432 | void P1Mini::ChangeState(enum states new_state) 433 | { 434 | unsigned long const current_time{ millis() }; 435 | switch (new_state) { 436 | case states::IDENTIFYING_MESSAGE: 437 | m_identifying_message_time = current_time; 438 | m_crc_position = m_message_buffer_position = 0; 439 | m_num_message_loops = m_num_processing_loops = 0; 440 | m_data_format = data_formats::UNKNOWN; 441 | for (auto T : m_ready_to_receive_triggers) T->trigger(); 442 | break; 443 | case states::READING_MESSAGE: 444 | m_reading_message_time = current_time; 445 | for (auto T : m_receiving_update_triggers) T->trigger(); 446 | break; 447 | case states::VERIFYING_CRC: 448 | m_verifying_crc_time = current_time; 449 | for (auto T : m_update_received_triggers) T->trigger(); 450 | break; 451 | case states::PROCESSING_ASCII: 452 | case states::PROCESSING_BINARY: 453 | m_processing_time = current_time; 454 | m_start_of_data = m_message_buffer; 455 | break; 456 | case states::WAITING: 457 | if (m_state != states::ERROR_RECOVERY) { 458 | m_display_time_stats = true; 459 | for (auto T : m_update_processed_triggers) T->trigger(); 460 | } 461 | m_waiting_time = current_time; 462 | break; 463 | case states::ERROR_RECOVERY: 464 | m_error_recovery_time = current_time; 465 | for (auto T : m_communication_error_triggers) T->trigger(); 466 | } 467 | m_state = new_state; 468 | } 469 | 470 | void P1Mini::AddByteToDiscardLog(uint8_t byte) 471 | { 472 | constexpr char hex_chars[] = "0123456789abcdef"; 473 | *m_discard_log_position++ = hex_chars[byte >> 4]; 474 | *m_discard_log_position++ = hex_chars[byte & 0xf]; 475 | if (m_discard_log_position == m_discard_log_end) FlushDiscardLog(); 476 | } 477 | 478 | void P1Mini::FlushDiscardLog() 479 | { 480 | if (m_discard_log_position != m_discard_log_buffer) { 481 | *m_discard_log_position = '\0'; 482 | ESP_LOGW(TAG, "Discarding: %s", m_discard_log_buffer); 483 | m_discard_log_position = m_discard_log_buffer; 484 | } 485 | } 486 | 487 | 488 | void P1Mini::dump_config() { 489 | ESP_LOGCONFIG(TAG, "P1 Mini component"); 490 | } 491 | 492 | } // namespace p1_mini 493 | } // namespace esphome -------------------------------------------------------------------------------- /components/p1_mini/p1_mini.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/uart/uart.h" 5 | #include "esphome/core/automation.h" 6 | 7 | #include 8 | 9 | namespace esphome { 10 | namespace p1_mini { 11 | 12 | 13 | class IP1MiniSensor 14 | { 15 | public: 16 | virtual ~IP1MiniSensor() = default; 17 | virtual void publish_val(double) = 0; 18 | virtual uint32_t Obis() const = 0; 19 | }; 20 | 21 | class P1MiniSensorBase : public IP1MiniSensor 22 | { 23 | uint32_t const m_obis; 24 | public: 25 | P1MiniSensorBase(std::string obis_code); 26 | virtual uint32_t Obis() const { return m_obis; } 27 | }; 28 | 29 | class IP1MiniTextSensor 30 | { 31 | public: 32 | virtual ~IP1MiniTextSensor() = default; 33 | virtual void publish_val(std::string) = 0; 34 | virtual std::string Identifier() const = 0; 35 | }; 36 | 37 | class P1MiniTextSensorBase : public IP1MiniTextSensor 38 | { 39 | std::string const m_identifier; 40 | public: 41 | P1MiniTextSensorBase(std::string identifier); 42 | virtual std::string Identifier() const { return m_identifier; } 43 | }; 44 | 45 | class ReadyToReceiveTrigger : public Trigger<> { }; 46 | class ReceivingUpdateTrigger : public Trigger<> { }; 47 | class UpdateReceivedTrigger : public Trigger<> { }; 48 | class UpdateProcessedTrigger : public Trigger<> { }; 49 | class CommunicationErrorTrigger : public Trigger<> { }; 50 | 51 | class P1Mini : public uart::UARTDevice, public Component { 52 | public: 53 | P1Mini(uint32_t min_period_ms, int buffer_size, bool secondary_p1); 54 | 55 | void setup() override; 56 | void loop() override; 57 | void dump_config() override; 58 | 59 | void register_sensor(IP1MiniSensor *sensor) 60 | { 61 | m_sensors.emplace(sensor->Obis(), sensor); 62 | } 63 | 64 | void register_text_sensor(IP1MiniTextSensor *sensor) 65 | { 66 | // Sort long identifiers first in the vector 67 | auto iter{ m_text_sensors.begin() }; 68 | while (iter != m_text_sensors.end() && sensor->Identifier().size() < (*iter)->Identifier().size()) ++iter; 69 | m_text_sensors.insert(iter, sensor); 70 | } 71 | 72 | void register_ready_to_receive_trigger(ReadyToReceiveTrigger *trigger) { m_ready_to_receive_triggers.push_back(trigger); } 73 | void register_receiving_update_trigger(ReceivingUpdateTrigger *trigger) { m_receiving_update_triggers.push_back(trigger); } 74 | void register_update_received_trigger(UpdateReceivedTrigger *trigger) { m_update_received_triggers.push_back(trigger); } 75 | void register_update_processed_trigger(UpdateProcessedTrigger *trigger) { m_update_processed_triggers.push_back(trigger); } 76 | void register_communication_error_trigger(CommunicationErrorTrigger *trigger) { m_communication_error_triggers.push_back(trigger); } 77 | 78 | private: 79 | 80 | unsigned long m_identifying_message_time{ 0 }; 81 | unsigned long m_reading_message_time{ 0 }; 82 | unsigned long m_verifying_crc_time{ 0 }; 83 | unsigned long m_processing_time{ 0 }; 84 | unsigned long m_waiting_time{ 0 }; 85 | unsigned long m_error_recovery_time{ 0 }; 86 | int m_num_message_loops{ 0 }; 87 | int m_num_processing_loops{ 0 }; 88 | bool m_display_time_stats{ false }; 89 | uint32_t m_time_stats_as_info_next{ 4 }; // 0 to disable 90 | uint32_t m_time_stats_counter{ 0 }; 91 | uint32_t m_obis_code{ 0 }; 92 | 93 | // Store the message as it is being received: 94 | std::unique_ptr m_message_buffer_UP; 95 | int m_message_buffer_size; 96 | char *m_message_buffer{ nullptr }; 97 | int m_message_buffer_position{ 0 }; 98 | int m_crc_position{ 0 }; 99 | 100 | // Keeps track of the start of the data record while processing. 101 | char *m_start_of_data; 102 | 103 | char GetByte() 104 | { 105 | char const C{ static_cast(read()) }; 106 | if (m_secondary_p1) write(C); 107 | return C; 108 | } 109 | 110 | enum class states { 111 | IDENTIFYING_MESSAGE, 112 | READING_MESSAGE, 113 | VERIFYING_CRC, 114 | PROCESSING_ASCII, 115 | PROCESSING_BINARY, 116 | WAITING, 117 | ERROR_RECOVERY 118 | }; 119 | enum states m_state { states::ERROR_RECOVERY }; 120 | 121 | void ChangeState(enum states new_state); 122 | 123 | enum class data_formats { 124 | UNKNOWN, 125 | ASCII, 126 | BINARY 127 | }; 128 | enum data_formats m_data_format { data_formats::UNKNOWN }; 129 | 130 | uint32_t const m_min_period_ms; 131 | bool const m_secondary_p1; 132 | 133 | std::map m_sensors; 134 | std::vector m_text_sensors; // Keep sorted so longer identifiers are first! 135 | 136 | std::vector m_ready_to_receive_triggers; 137 | std::vector m_receiving_update_triggers; 138 | std::vector m_update_received_triggers; 139 | std::vector m_update_processed_triggers; 140 | std::vector m_communication_error_triggers; 141 | 142 | constexpr static int discard_log_num_bytes{ 32 }; 143 | char m_discard_log_buffer[discard_log_num_bytes * 2 + 1]; 144 | char *m_discard_log_position{ m_discard_log_buffer }; 145 | char *const m_discard_log_end{ m_discard_log_buffer + (discard_log_num_bytes * 2) }; 146 | 147 | void AddByteToDiscardLog(uint8_t byte); 148 | void FlushDiscardLog(); 149 | 150 | }; 151 | 152 | 153 | } // namespace p1_mini_component 154 | } // namespace esphome 155 | -------------------------------------------------------------------------------- /components/p1_mini/sensor/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor 4 | from esphome.const import CONF_FORMAT, CONF_ID, CONF_TIMEOUT 5 | 6 | from .. import CONF_P1_MINI_ID, CONF_OBIS_CODE, P1Mini, obis_code, p1_mini_ns 7 | 8 | AUTO_LOAD = ["p1_mini"] 9 | 10 | P1MiniSensor = p1_mini_ns.class_( 11 | "P1MiniSensor", sensor.Sensor, cg.Component) 12 | 13 | CONFIG_SCHEMA = sensor.sensor_schema(P1MiniSensor).extend( 14 | { 15 | cv.GenerateID(): cv.declare_id(P1MiniSensor), 16 | cv.GenerateID(CONF_P1_MINI_ID): cv.use_id(P1Mini), 17 | cv.Required(CONF_OBIS_CODE): cv.string 18 | } 19 | ) 20 | 21 | async def to_code(config): 22 | var = cg.new_Pvariable( 23 | config[CONF_ID], 24 | config[CONF_OBIS_CODE], 25 | ) 26 | await cg.register_component(var, config) 27 | await sensor.register_sensor(var, config) 28 | p1_mini = await cg.get_variable(config[CONF_P1_MINI_ID]) 29 | cg.add(p1_mini.register_sensor(var)) 30 | -------------------------------------------------------------------------------- /components/p1_mini/sensor/p1_mini_sensor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "esphome/core/log.h" 6 | 7 | //#include "../p1_mini.h" 8 | 9 | namespace esphome 10 | { 11 | namespace p1_mini 12 | { 13 | 14 | } // namespace p1_mini 15 | } // namespace esphome -------------------------------------------------------------------------------- /components/p1_mini/sensor/p1_mini_sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/components/sensor/sensor.h" 6 | 7 | #include "../p1_mini.h" 8 | 9 | namespace esphome 10 | { 11 | namespace p1_mini 12 | { 13 | class P1MiniSensor : public P1MiniSensorBase, public sensor::Sensor, public Component 14 | { 15 | public: 16 | P1MiniSensor(std::string obis_code) 17 | : P1MiniSensorBase{ obis_code } 18 | {} 19 | 20 | virtual void publish_val(double value) override { publish_state(value); } 21 | 22 | }; 23 | 24 | } // namespace p1_mini 25 | } // namespace esphome 26 | -------------------------------------------------------------------------------- /components/p1_mini/text_sensor/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import text_sensor 4 | from esphome.const import CONF_FORMAT, CONF_ID, CONF_TIMEOUT 5 | 6 | from .. import CONF_P1_MINI_ID, CONF_IDENTIFIER, P1Mini, identifier, p1_mini_ns 7 | 8 | AUTO_LOAD = ["p1_mini"] 9 | 10 | P1MiniTextSensor = p1_mini_ns.class_( 11 | "P1MiniTextSensor", text_sensor.TextSensor, cg.Component) 12 | 13 | CONFIG_SCHEMA = text_sensor.text_sensor_schema(P1MiniTextSensor).extend( 14 | { 15 | cv.GenerateID(): cv.declare_id(P1MiniTextSensor), 16 | cv.GenerateID(CONF_P1_MINI_ID): cv.use_id(P1Mini), 17 | cv.Required(CONF_IDENTIFIER): cv.string 18 | } 19 | ) 20 | 21 | async def to_code(config): 22 | var = cg.new_Pvariable( 23 | config[CONF_ID], 24 | config[CONF_IDENTIFIER], 25 | ) 26 | await cg.register_component(var, config) 27 | await text_sensor.register_text_sensor(var, config) 28 | p1_mini = await cg.get_variable(config[CONF_P1_MINI_ID]) 29 | cg.add(p1_mini.register_text_sensor(var)) 30 | -------------------------------------------------------------------------------- /components/p1_mini/text_sensor/p1_mini_text_sensor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "esphome/core/log.h" 6 | 7 | //#include "../p1_mini.h" 8 | 9 | namespace esphome 10 | { 11 | namespace p1_mini 12 | { 13 | 14 | } // namespace p1_mini 15 | } // namespace esphome -------------------------------------------------------------------------------- /components/p1_mini/text_sensor/p1_mini_text_sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/components/sensor/sensor.h" 6 | 7 | #include "../p1_mini.h" 8 | 9 | namespace esphome 10 | { 11 | namespace p1_mini 12 | { 13 | class P1MiniTextSensor : public P1MiniTextSensorBase, public text_sensor::TextSensor, public Component 14 | { 15 | public: 16 | P1MiniTextSensor(std::string identifier) 17 | : P1MiniTextSensorBase{ identifier } 18 | {} 19 | 20 | virtual void publish_val(std::string value) override { publish_state(value); } 21 | 22 | }; 23 | 24 | } // namespace p1_mini 25 | } // namespace esphome 26 | -------------------------------------------------------------------------------- /docs/NO-RTS.md: -------------------------------------------------------------------------------- 1 | # The RTS signal 2 | p1mini uses the RTS signal to request updates from the meter at the desired intervals. Once an update has been received, RTS is set low while the data is processed, which insures that the meter will not send new updates before the p1mini is ready to receive. 3 | 4 | Another advantage is that some meters can send updates more frequently when prompted by the RTS signal than they do when RTS is left always high. 5 | 6 | ## RTS always high 7 | If the RTS signal is always high, the meter will send updates at some interval which is specific to that meter (1 second, 5 seconds and 10 seconds have been observerd). The meter will send the update regardless if the p1mini is ready to receive or not (but a typical p1mini can receive and process more than two updates per second, so usually not a problem) 8 | 9 | Generally, the RTS signal is a good thing, but there are some cases when it is better to set it high all the time: If the meter works unreliably with the RTS signal or if your p1mini was build without attaching the RTS signal to a GPIO pin. 10 | 11 | ### Meters that do not work well with RTS 12 | The following meters have been known to have issues with RTS. 13 | 14 | #### S34U18 (Sanxing SX631) 15 | The S34U18 seems buggy and may stop working intermittently when using the RTS signal. If you are having issues, try keeping RTS high all the time in the config. 16 | 17 | #### KAIFA MA304T4E / MA304H4E 18 | May not work at all wihtout setting RTS constantly high. 19 | 20 | #### Landis+Gyr E360 21 | Most meters accepth 3.3 V on the RTS signal, but the E360 may need the full 5 volts, which is according to specifications, so there is nothing wrong with the meter. This can be solved with level shifting circuitry or by simply attaching RTS directly to 5 V all the time. (And setting the minimum period to 0s) 22 | 23 | ### RTS not attached to a GPIO 24 | If you built your ESP based on the original esphome-p1reader or if you bought a Slimmeleser etc which does not connect the RTS to a GPIO, you need to set the minimum update period to zero. 25 | 26 | ## Disabling the RTS signal 27 | By changing this, the RTS signal (if connected) will be set constantly high, and the p1mini will work slightly differently internally. 28 | 29 | In the yaml file, never set p1_rts low. Simply find and remove this line (in two places): 30 | 31 | ``` 32 | - switch.turn_off: p1_rts 33 | ``` 34 | 35 | If RTS is not connected to a GPIO, p1_rts and all references to it can be removed from the yaml. 36 | 37 | Also, the minimum update period needs to be set to zero: 38 | 39 | ``` 40 | minimum_period: 0s 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/build_c3_zero.md: -------------------------------------------------------------------------------- 1 | # Build instructions for (Waveshare) ESP32-C3-Zero 2 | 3 | - 1 [(Waveshare) ESP32-C3-Zero](https://www.waveshare.com/wiki/ESP32-C3-Zero) or clone. 4 | - 1 RJ12 connector and cable (6 wires) 5 | - 1 Resistor in the range 1 - 4 kΩ is recommended. 6 | - Optionally, hot melt glue and large heat shrink tubing. 7 | 8 | Connect according to this image: 9 | 10 | ![RJ12 pins](../images/C3-wiring.jpg) 11 | 12 | It is fairly likely that the internal pull-up resistor will not be effective at the data rates used by the serial communication. 13 | Adding an external pull-up resistor between 3.3V and GPIO1 will avoid the risk of having to troubleshoot a likely problem: 14 | 15 | ![RJ12 pins](../images/C3-pullup.jpg) 16 | 17 | Some hot-melt glue and heat shrink tubing will make it more robust: 18 | 19 | ![Completed](../images/C3-complete.jpg) 20 | 21 | -------------------------------------------------------------------------------- /docs/build_d1_mini.md: -------------------------------------------------------------------------------- 1 | # Build instructions for (Wemos) D1 mini 2 | 3 | - 1 [(Wemos) D1 mini](https://www.wemos.cc/en/latest/d1/d1_mini.html) or clone. 4 | - 1 RJ12 connector and cable (6 wires) 5 | - Optionally, hot melt glue and large heat shrink tubing. 6 | 7 | Wiring is simple. Five of the pins from the connector (one pin is not used)... 8 | 9 | ![RJ12 pins](../images/RJ12-pins.png) 10 | 11 | ... are connected to four of the pads on the D1 mini. 12 | 13 | ![D1 mini pins](../images/D1mini-pins.png) 14 | 15 | And that is it. The result could look something like this: 16 | 17 | ![Soldered](../images/soldered.png) 18 | 19 | Some hot-melt glue and heat shrink tubing will make it more robust though. 20 | 21 | ![Completed](../images/completed.png) 22 | -------------------------------------------------------------------------------- /docs/component_only.md: -------------------------------------------------------------------------------- 1 | # Using the p1_mini component in your own config file 2 | The `p1_mini` component and sensor can be used from any ESPHome config file by adding the Github repository as source for external components: 3 | 4 | ``` 5 | external_components: 6 | - source: github://Beaky2000/esphome-p1mini@main 7 | ``` 8 | 9 | One drawback of using this method, instead of using the config file included in this project, is that your config can stop working at any time because of updates to the component that you need to account for in your config. Therefore this makes most sense if you are making large modifications to the config, such as using some other hardware than a D1 mini etc. 10 | 11 | For an example of how to set up UARTs, the p1_mini component and sensors, look at the included config file. Some day I might document all parameters here, but that day is not yet here. 12 | -------------------------------------------------------------------------------- /docs/passthrough.md: -------------------------------------------------------------------------------- 1 | # P1 Passthrough 2 | It is possible to attach another P1 reading device in case you need to connect a car charger (or a second p1-mini...) etc. 3 | 4 | > [!WARNING] 5 | > Currently, the RTS signal of the secondary device is ignored. If you are using the secondary port *and* this is a problem, let me know and I will try to fix it! 6 | 7 | ### Parts 8 | - Female connector for the RJ12 cable. 9 | - White (or blue) LED 10 | - Resistor 1 - 3 kΩ 11 | 12 | ![Secondary port pins](../images/secondary_pins.png) 13 | 14 | > [!NOTE] 15 | > Since the RTS signal is currenty ignored, only the TX and GND pins need to be connected. TX -> TX and GND -> Data GND. (TX -> GPIO1 on the ESP32-C3-Zero) 16 | 17 | The LED will, in addition to providing visual indication that updates are beeing requested on the port, ensure that the voltage on D0 will not get high enough to damage the D1 mini. The LED needs to have a high enough voltage drop for it to work and some colors may not work. 18 | 19 | The value of the resistor is not very critical. I have tested with 3 kΩ and anything down to 1 kΩ should be fine. 20 | 21 | A p1mini wired up with a secondary port (unpowered) on an experimental board: 22 | 23 | ![Secondary port](../images/secondary_experimental.png) 24 | 25 | ### Power to the secondary port 26 | 27 | Power to the secondary port needs to be supplied from a secondary source (such as an USB charger). Unless the secondary device is already powered (like a car charger etc) in which case it may not be necessary to supply any power at all to the secondary port. 28 | 29 | ### Configuration changes 30 | The feature needs to be enabled in the configuration file (yaml). Change `secondary_p1` from `false` to `true`. 31 | ``` 32 | secondary_p1: true 33 | ``` 34 | 35 | ### Limitations 36 | 37 | The RTS signal of the secondaty port is ignored and all data is passed along as soon as it is received, regardless if the secondary device is ready to receive or not. 38 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | Things to check if something is not working: 3 | 4 | ## WiFi 5 | Make sure the p1mini is connecting to your WiFi: 6 | * Access the web browser on the P1mini: (usually) http://p1mini.local/ or http://p1mini32.local/ 7 | * Temporarily move it closer to the router, in case it is out of range. 8 | * etc 9 | 10 | ## Log 11 | Check the log (and probably set the log level to DEBUG in the yaml file). 12 | 13 | ### No data received... 14 | If you see `No data received for 60 seconds.` in the log, no data at all is beeing received. 15 | * Verify that the port is enabled. Check with the supplier. 16 | * Check that the correct pin is soldered in the right place. 17 | * If you can, inspect the signal with an oscilloscope. The signal should consist of distinct pulses at around 3.3V with the shortest pulses beeing roughly 10µs: 18 | 19 | ![Good signal](../images/signal-good.jpg) 20 | 21 | ### Unknown data format... 22 | If you see `Unknown data format (0x??). Resetting.`, followed by a lot of `Discarding: ...`, then data is beeing received but it is incorrect in some way. 23 | 24 | #### Double inverted signal 25 | If you are inverting the signal in hardware (using a transistor etc) make sure that you are not also inverting in the yaml. 26 | 27 | #### Insufficient pull-up resistor 28 | If the discarded data contains a lot of "weird" sequences, like `c3e3c3c3c3c3`, `f8f8ffff`, etc, it is likely that the internal pullup resistor in the ESP chip is not effective at the data rates used by the serial communication. When looking at the signal it might look like this: 29 | 30 | ![Bad signal](../images/signal-bad.jpg) 31 | 32 | In many cases, this can be fixed by adding an external resistor between 3.3V and the input pin (RX on a D1 mini, GPIO1 on the ESP32-C3-Zero) on the ESP module. Lower resistances are more "aggressive" and I would not recommend going below 500Ω but in many cases values as high as 5kΩ work fine! 33 | 34 | -------------------------------------------------------------------------------- /images/C3-complete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/C3-complete.jpg -------------------------------------------------------------------------------- /images/C3-pullup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/C3-pullup.jpg -------------------------------------------------------------------------------- /images/C3-wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/C3-wiring.jpg -------------------------------------------------------------------------------- /images/D1mini-pins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/D1mini-pins.png -------------------------------------------------------------------------------- /images/RJ12-pins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/RJ12-pins.png -------------------------------------------------------------------------------- /images/completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/completed.png -------------------------------------------------------------------------------- /images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/header.jpg -------------------------------------------------------------------------------- /images/inHA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/inHA.png -------------------------------------------------------------------------------- /images/secondary_experimental.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/secondary_experimental.png -------------------------------------------------------------------------------- /images/secondary_pins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/secondary_pins.png -------------------------------------------------------------------------------- /images/signal-bad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/signal-bad.jpg -------------------------------------------------------------------------------- /images/signal-good.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/signal-good.jpg -------------------------------------------------------------------------------- /images/soldered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beaky2000/esphome-p1mini/cb7525917111de23b5ae8e3c36ce4fe719cde186/images/soldered.png -------------------------------------------------------------------------------- /p1mini.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: p1mini 3 | device_password: !secret p1mini_password 4 | device_api_key: !secret p1mini_api_key 5 | 6 | esphome: 7 | name: ${device_name} 8 | 9 | esp8266: 10 | board: d1_mini 11 | 12 | wifi: 13 | ssid: !secret wifi_ssid 14 | password: !secret wifi_password 15 | 16 | # Enable fallback hotspot (captive portal) in case wifi connection fails 17 | ap: 18 | ssid: "esp-${device_name}" 19 | password: "${device_password}" 20 | 21 | captive_portal: 22 | 23 | # Enable logging 24 | logger: 25 | level: INFO # Set to DEBUG if you are having issues! 26 | baud_rate: 0 # disable logging over uart 27 | 28 | # Enable Home Assistant API 29 | api: 30 | #password: "${device_password}" ### Deprecated 31 | encryption: 32 | key: "${device_api_key}" 33 | 34 | ota: 35 | platform: esphome # Remove this line to run on ESPHome versions earlier than 2024.6.0 36 | password: "${device_password}" 37 | 38 | web_server: 39 | port: 80 40 | ota: false 41 | # auth: 42 | # username: admin 43 | # password: "${device_password}" 44 | 45 | external_components: 46 | - source: components 47 | 48 | switch: 49 | - platform: gpio 50 | id: p1_rts # Not needed if the RTS signal is not connected to a GPIO 51 | pin: 52 | number: D2 53 | - platform: gpio 54 | id: status_led 55 | pin: 56 | number: D4 57 | inverted: true 58 | 59 | binary_sensor: 60 | - platform: gpio 61 | id: secondary_p1_rts # Currently not used, but make sure D0 is configured as input in case it is connected. 62 | pin: 63 | number: D0 64 | mode: INPUT_PULLDOWN 65 | inverted: false 66 | 67 | uart: 68 | - id: my_uart_1 69 | tx_pin: 70 | number: TX 71 | inverted: true 72 | mode: OUTPUT_OPEN_DRAIN 73 | rx_pin: 74 | number: RX 75 | inverted: true # Set to false if inverting in hardware 76 | mode: INPUT_PULLUP # Set to INPUT if inverting in hardware 77 | baud_rate: 115200 78 | rx_buffer_size: 512 # Probably not needed, but it is good to have some margin. 79 | # - id: my_uart_2 80 | # ... 81 | 82 | p1_mini: 83 | - id: p1_mini_1 84 | uart_id: my_uart_1 85 | minimum_period: 2s # Should be 0 (zero) if the RTS signal is not used. 86 | buffer_size: 3072 # Needs to be large enough to hold one entire update from the meter. 87 | secondary_p1: false 88 | on_ready_to_receive: 89 | then: 90 | - switch.turn_on: p1_rts 91 | - switch.turn_on: status_led 92 | on_receiving_update: 93 | then: 94 | on_update_received: 95 | then: 96 | - switch.turn_off: p1_rts 97 | - switch.turn_off: status_led 98 | on_update_processed: 99 | then: 100 | on_communication_error: 101 | then: 102 | - switch.turn_off: p1_rts 103 | - switch.turn_off: status_led 104 | # - id: p1_mini_2 105 | # uart_id: my_uart_2 106 | # ... 107 | 108 | text_sensor: 109 | - platform: p1_mini 110 | name: "Meter ID" 111 | icon: "mdi:identifier" 112 | p1_mini_id: p1_mini_1 113 | identifier: "/" 114 | filters: 115 | - substitute: "/ -> " 116 | # - platform: p1_mini 117 | # name: "Date+Time" 118 | # icon: "mdi:calendar-range" 119 | # p1_mini_id: p1_mini_1 120 | # identifier: "0-0:1.0.0(" 121 | # filters: 122 | # - substitute: "0-0:1.0.0( -> " 123 | # - substitute: ") -> " 124 | sensor: 125 | - platform: wifi_signal 126 | name: "${device_name} WiFi Signal" 127 | update_interval: 10s 128 | - platform: p1_mini 129 | p1_mini_id: p1_mini_1 130 | obis_code: "1.8.0" 131 | name: "Cumulative Active Import" 132 | icon: "mdi:transmission-tower-export" 133 | unit_of_measurement: kWh 134 | accuracy_decimals: 3 135 | state_class: "total_increasing" 136 | device_class: "energy" 137 | - platform: p1_mini 138 | p1_mini_id: p1_mini_1 139 | obis_code: "2.8.0" 140 | name: "Cumulative Active Export" 141 | icon: "mdi:transmission-tower-import" 142 | unit_of_measurement: kWh 143 | accuracy_decimals: 3 144 | state_class: "total_increasing" 145 | device_class: "energy" 146 | - platform: p1_mini 147 | p1_mini_id: p1_mini_1 148 | obis_code: "3.8.0" 149 | name: "Cumulative Reactive Import" 150 | icon: "mdi:transmission-tower-export" 151 | unit_of_measurement: kvarh 152 | accuracy_decimals: 3 153 | - platform: p1_mini 154 | p1_mini_id: p1_mini_1 155 | obis_code: "4.8.0" 156 | name: "Cumulative Reactive Export" 157 | icon: "mdi:transmission-tower-import" 158 | unit_of_measurement: kvarh 159 | accuracy_decimals: 3 160 | - platform: p1_mini 161 | p1_mini_id: p1_mini_1 162 | obis_code: "1.7.0" 163 | name: "Momentary Active Import" 164 | icon: "mdi:transmission-tower-export" 165 | unit_of_measurement: kW 166 | accuracy_decimals: 3 167 | device_class: "power" 168 | state_class: "measurement" 169 | - platform: p1_mini 170 | p1_mini_id: p1_mini_1 171 | obis_code: "2.7.0" 172 | name: "Momentary Active Export" 173 | icon: "mdi:transmission-tower-import" 174 | unit_of_measurement: kW 175 | accuracy_decimals: 3 176 | device_class: "power" 177 | state_class: "measurement" 178 | - platform: p1_mini 179 | p1_mini_id: p1_mini_1 180 | obis_code: "3.7.0" 181 | name: "Momentary Reactive Import" 182 | icon: "mdi:transmission-tower-export" 183 | unit_of_measurement: kvar 184 | accuracy_decimals: 3 185 | - platform: p1_mini 186 | p1_mini_id: p1_mini_1 187 | obis_code: "4.7.0" 188 | name: "Momentary Reactive Export" 189 | icon: "mdi:transmission-tower-import" 190 | unit_of_measurement: kvar 191 | accuracy_decimals: 3 192 | - platform: p1_mini 193 | p1_mini_id: p1_mini_1 194 | obis_code: "21.7.0" 195 | name: "Momentary Active Import Phase 1" 196 | icon: "mdi:transmission-tower-export" 197 | unit_of_measurement: kW 198 | accuracy_decimals: 3 199 | device_class: "power" 200 | state_class: "measurement" 201 | - platform: p1_mini 202 | p1_mini_id: p1_mini_1 203 | obis_code: "22.7.0" 204 | name: "Momentary Active Export Phase 1" 205 | icon: "mdi:transmission-tower-import" 206 | unit_of_measurement: kW 207 | accuracy_decimals: 3 208 | device_class: "power" 209 | state_class: "measurement" 210 | - platform: p1_mini 211 | p1_mini_id: p1_mini_1 212 | obis_code: "41.7.0" 213 | name: "Momentary Active Import Phase 2" 214 | icon: "mdi:transmission-tower-export" 215 | unit_of_measurement: kW 216 | accuracy_decimals: 3 217 | device_class: "power" 218 | state_class: "measurement" 219 | - platform: p1_mini 220 | p1_mini_id: p1_mini_1 221 | obis_code: "42.7.0" 222 | name: "Momentary Active Export Phase 2" 223 | icon: "mdi:transmission-tower-import" 224 | unit_of_measurement: kW 225 | accuracy_decimals: 3 226 | device_class: "power" 227 | state_class: "measurement" 228 | - platform: p1_mini 229 | p1_mini_id: p1_mini_1 230 | obis_code: "61.7.0" 231 | name: "Momentary Active Import Phase 3" 232 | icon: "mdi:transmission-tower-export" 233 | unit_of_measurement: kW 234 | accuracy_decimals: 3 235 | device_class: "power" 236 | state_class: "measurement" 237 | - platform: p1_mini 238 | p1_mini_id: p1_mini_1 239 | obis_code: "62.7.0" 240 | name: "Momentary Active Export Phase 3" 241 | icon: "mdi:transmission-tower-import" 242 | unit_of_measurement: kW 243 | accuracy_decimals: 3 244 | device_class: "power" 245 | state_class: "measurement" 246 | - platform: p1_mini 247 | p1_mini_id: p1_mini_1 248 | obis_code: "23.7.0" 249 | name: "Momentary Reactive Import Phase 1" 250 | icon: "mdi:transmission-tower-export" 251 | unit_of_measurement: kvar 252 | accuracy_decimals: 3 253 | - platform: p1_mini 254 | p1_mini_id: p1_mini_1 255 | obis_code: "24.7.0" 256 | name: "Momentary Reactive Export Phase 1" 257 | icon: "mdi:transmission-tower-import" 258 | unit_of_measurement: kvar 259 | accuracy_decimals: 3 260 | - platform: p1_mini 261 | p1_mini_id: p1_mini_1 262 | obis_code: "43.7.0" 263 | name: "Momentary Reactive Import Phase 2" 264 | icon: "mdi:transmission-tower-export" 265 | unit_of_measurement: kvar 266 | accuracy_decimals: 3 267 | - platform: p1_mini 268 | p1_mini_id: p1_mini_1 269 | obis_code: "44.7.0" 270 | name: "Momentary Reactive Export Phase 2" 271 | icon: "mdi:transmission-tower-import" 272 | unit_of_measurement: kvar 273 | accuracy_decimals: 3 274 | - platform: p1_mini 275 | p1_mini_id: p1_mini_1 276 | obis_code: "63.7.0" 277 | name: "Momentary Reactive Import Phase 3" 278 | icon: "mdi:transmission-tower-export" 279 | unit_of_measurement: kvar 280 | accuracy_decimals: 3 281 | - platform: p1_mini 282 | p1_mini_id: p1_mini_1 283 | obis_code: "64.7.0" 284 | name: "Momentary Reactive Export Phase 3" 285 | icon: "mdi:transmission-tower-import" 286 | unit_of_measurement: kvar 287 | accuracy_decimals: 3 288 | - platform: p1_mini 289 | p1_mini_id: p1_mini_1 290 | obis_code: "32.7.0" 291 | name: "Voltage Phase 1" 292 | icon: "mdi:lightning-bolt-outline" 293 | unit_of_measurement: V 294 | accuracy_decimals: 1 295 | device_class: "voltage" 296 | state_class: "measurement" 297 | - platform: p1_mini 298 | p1_mini_id: p1_mini_1 299 | obis_code: "52.7.0" 300 | name: "Voltage Phase 2" 301 | icon: "mdi:lightning-bolt-outline" 302 | unit_of_measurement: V 303 | accuracy_decimals: 1 304 | device_class: "voltage" 305 | state_class: "measurement" 306 | - platform: p1_mini 307 | p1_mini_id: p1_mini_1 308 | obis_code: "72.7.0" 309 | name: "Voltage Phase 3" 310 | icon: "mdi:lightning-bolt-outline" 311 | unit_of_measurement: V 312 | accuracy_decimals: 1 313 | device_class: "voltage" 314 | state_class: "measurement" 315 | - platform: p1_mini 316 | p1_mini_id: p1_mini_1 317 | obis_code: "31.7.0" 318 | name: "Current Phase 1" 319 | icon: "mdi:lightning-bolt" 320 | unit_of_measurement: A 321 | accuracy_decimals: 1 322 | device_class: "current" 323 | state_class: "measurement" 324 | - platform: p1_mini 325 | p1_mini_id: p1_mini_1 326 | obis_code: "51.7.0" 327 | name: "Current Phase 2" 328 | icon: "mdi:lightning-bolt" 329 | unit_of_measurement: A 330 | accuracy_decimals: 1 331 | device_class: "current" 332 | state_class: "measurement" 333 | - platform: p1_mini 334 | p1_mini_id: p1_mini_1 335 | obis_code: "71.7.0" 336 | name: "Current Phase 3" 337 | icon: "mdi:lightning-bolt" 338 | unit_of_measurement: A 339 | accuracy_decimals: 1 340 | device_class: "current" 341 | state_class: "measurement" 342 | # - platform: p1_mini 343 | # p1_mini_id: p1_mini_2 344 | # obis_code: "1.8.0" 345 | # name: "Cumulative Active Import, meter 2" 346 | # unit_of_measurement: kWh 347 | # accuracy_decimals: 3 348 | # state_class: "total_increasing" 349 | # device_class: "energy" 350 | # ... 351 | -------------------------------------------------------------------------------- /p1mini32.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for a Waveshare ESP32-C3-Zero module ( https://www.waveshare.com/wiki/ESP32-C3-Zero ) 2 | # 3 | # P1-port C3 4 | # ----------------------- 5 | # 1 - 5V 5V 6 | # 2 - RTS GPIO0 7 | # 3 - Data GND GND 8 | # 4 - N/C 9 | # 5 - TX GPIO1 10 | # 6 - GND GND 11 | # 12 | # Also a 3.3 kohm resistor between 3.3V and GPIO1 (anything in the range 1 - 4 kohm is probably fine) since the internal 13 | # pullup resistor in this chip does not seem to be effective at the data rates used by the serial communication. 14 | # 15 | # The Waveshare module has a WS2812 multi-color LED on GPIO10 which provides colorful feedback! 16 | # 17 | substitutions: 18 | device_name: p1mini32 19 | device_password: !secret p1mini_password 20 | device_api_key: !secret p1mini_api_key 21 | 22 | esphome: 23 | name: ${device_name} 24 | 25 | esp32: 26 | board: esp32-c3-devkitm-1 27 | framework: 28 | type: esp-idf 29 | 30 | wifi: 31 | ssid: !secret wifi_ssid 32 | password: !secret wifi_password 33 | 34 | # Enable fallback hotspot (captive portal) in case wifi connection fails 35 | ap: 36 | ssid: "esp-${device_name}" 37 | password: "${device_password}" 38 | 39 | captive_portal: 40 | 41 | # Enable logging 42 | logger: 43 | level: INFO # Set to DEBUG if you are having issues! 44 | #baud_rate: 0 # disable logging over uart (or not, the C3 has two uarts) 45 | 46 | api: 47 | encryption: 48 | key: "${device_api_key}" 49 | 50 | ota: 51 | platform: esphome 52 | password: "${device_password}" 53 | 54 | web_server: 55 | port: 80 56 | ota: false 57 | # auth: 58 | # username: admin 59 | # password: "${device_password}" 60 | 61 | external_components: 62 | - source: components 63 | 64 | light: 65 | - platform: esp32_rmt_led_strip 66 | rgb_order: RGB # Some clone boards have color in a different order, such as GRB! 67 | chipset: ws2812 68 | pin: GPIO10 69 | gamma_correct: 1.0 70 | color_correct: [ 100%, 60%, 50% ] 71 | num_leds: 1 72 | id: status_light 73 | 74 | script: 75 | - id: status_light_color 76 | parameters: 77 | param_brightness: float 78 | param_red: float 79 | param_green: float 80 | param_blue: float 81 | then: 82 | - light.turn_on: 83 | id: status_light 84 | brightness: !lambda return param_brightness; 85 | red: !lambda return param_red; 86 | green: !lambda return param_green; 87 | blue: !lambda return param_blue; 88 | transition_length: 0s 89 | 90 | switch: 91 | - platform: gpio 92 | id: p1_rts 93 | pin: 94 | number: GPIO0 95 | 96 | uart: 97 | - id: my_uart_1 98 | tx_pin: 99 | number: GPIO3 100 | inverted: true 101 | mode: OUTPUT_OPEN_DRAIN 102 | rx_pin: 103 | number: GPIO1 104 | inverted: true # Set to false if inverting in hardware 105 | mode: INPUT_PULLUP # Set to INPUT if inverting in hardware 106 | baud_rate: 115200 107 | rx_buffer_size: 512 # Probably not needed, but it is good to have some margin. 108 | # - id: my_uart_2 109 | # ... 110 | 111 | p1_mini: 112 | - id: p1_mini_1 113 | uart_id: my_uart_1 114 | minimum_period: 2s # Should be 0 (zero) if the RTS signal is not used. 115 | buffer_size: 3072 # Needs to be large enough to hold one entire update from the meter. 116 | secondary_p1: false 117 | on_ready_to_receive: 118 | then: 119 | - switch.turn_on: p1_rts 120 | - lambda: id(status_light_color)->execute(0.33f, 0.0f, 0.0f, 1.0f); # Blue 121 | on_receiving_update: 122 | then: 123 | - lambda: id(status_light_color)->execute(0.25f, 1.0f, 1.0f, 1.0f); # White 124 | on_update_received: 125 | then: 126 | - switch.turn_off: p1_rts 127 | - lambda: id(status_light_color)->execute(0.30f, 1.0f, 0.8f, 0.0f); # Yellow/Orange 128 | on_update_processed: 129 | then: 130 | if: 131 | condition: 132 | wifi.connected: 133 | then: 134 | - lambda: id(status_light_color)->execute(0.33f, 0.0f, 1.0f, 0.0f); # Green 135 | else: 136 | - lambda: id(status_light_color)->execute(0.40f, 1.0f, 0.0f, 0.8f); # Purple 137 | on_communication_error: 138 | then: 139 | - switch.turn_off: p1_rts 140 | - lambda: id(status_light_color)->execute(0.50f, 1.0f, 0.0f, 0.0f); # Red 141 | # - id: p1_mini_2 142 | # uart_id: my_uart_2 143 | # ... 144 | 145 | text_sensor: 146 | - platform: p1_mini 147 | name: "Meter ID" 148 | icon: "mdi:identifier" 149 | p1_mini_id: p1_mini_1 150 | identifier: "/" 151 | filters: 152 | - substitute: "/ -> " 153 | # - platform: p1_mini 154 | # name: "Date+Time" 155 | # icon: "mdi:calendar-range" 156 | # p1_mini_id: p1_mini_1 157 | # identifier: "0-0:1.0.0(" 158 | # filters: 159 | # - substitute: "0-0:1.0.0( -> " 160 | # - substitute: ") -> " 161 | sensor: 162 | - platform: wifi_signal 163 | name: "${device_name} WiFi Signal" 164 | update_interval: 10s 165 | - platform: p1_mini 166 | p1_mini_id: p1_mini_1 167 | obis_code: "1.8.0" 168 | name: "Cumulative Active Import" 169 | icon: "mdi:transmission-tower-export" 170 | unit_of_measurement: kWh 171 | accuracy_decimals: 3 172 | state_class: "total_increasing" 173 | device_class: "energy" 174 | - platform: p1_mini 175 | p1_mini_id: p1_mini_1 176 | obis_code: "2.8.0" 177 | name: "Cumulative Active Export" 178 | icon: "mdi:transmission-tower-import" 179 | unit_of_measurement: kWh 180 | accuracy_decimals: 3 181 | state_class: "total_increasing" 182 | device_class: "energy" 183 | - platform: p1_mini 184 | p1_mini_id: p1_mini_1 185 | obis_code: "3.8.0" 186 | name: "Cumulative Reactive Import" 187 | icon: "mdi:transmission-tower-export" 188 | unit_of_measurement: kvarh 189 | accuracy_decimals: 3 190 | - platform: p1_mini 191 | p1_mini_id: p1_mini_1 192 | obis_code: "4.8.0" 193 | name: "Cumulative Reactive Export" 194 | icon: "mdi:transmission-tower-import" 195 | unit_of_measurement: kvarh 196 | accuracy_decimals: 3 197 | - platform: p1_mini 198 | p1_mini_id: p1_mini_1 199 | obis_code: "1.7.0" 200 | name: "Momentary Active Import" 201 | icon: "mdi:transmission-tower-export" 202 | unit_of_measurement: kW 203 | accuracy_decimals: 3 204 | device_class: "power" 205 | state_class: "measurement" 206 | - platform: p1_mini 207 | p1_mini_id: p1_mini_1 208 | obis_code: "2.7.0" 209 | name: "Momentary Active Export" 210 | icon: "mdi:transmission-tower-import" 211 | unit_of_measurement: kW 212 | accuracy_decimals: 3 213 | device_class: "power" 214 | state_class: "measurement" 215 | - platform: p1_mini 216 | p1_mini_id: p1_mini_1 217 | obis_code: "3.7.0" 218 | name: "Momentary Reactive Import" 219 | icon: "mdi:transmission-tower-export" 220 | unit_of_measurement: kvar 221 | accuracy_decimals: 3 222 | - platform: p1_mini 223 | p1_mini_id: p1_mini_1 224 | obis_code: "4.7.0" 225 | name: "Momentary Reactive Export" 226 | icon: "mdi:transmission-tower-import" 227 | unit_of_measurement: kvar 228 | accuracy_decimals: 3 229 | - platform: p1_mini 230 | p1_mini_id: p1_mini_1 231 | obis_code: "21.7.0" 232 | name: "Momentary Active Import Phase 1" 233 | icon: "mdi:transmission-tower-export" 234 | unit_of_measurement: kW 235 | accuracy_decimals: 3 236 | device_class: "power" 237 | state_class: "measurement" 238 | - platform: p1_mini 239 | p1_mini_id: p1_mini_1 240 | obis_code: "22.7.0" 241 | name: "Momentary Active Export Phase 1" 242 | icon: "mdi:transmission-tower-import" 243 | unit_of_measurement: kW 244 | accuracy_decimals: 3 245 | device_class: "power" 246 | state_class: "measurement" 247 | - platform: p1_mini 248 | p1_mini_id: p1_mini_1 249 | obis_code: "41.7.0" 250 | name: "Momentary Active Import Phase 2" 251 | icon: "mdi:transmission-tower-export" 252 | unit_of_measurement: kW 253 | accuracy_decimals: 3 254 | device_class: "power" 255 | state_class: "measurement" 256 | - platform: p1_mini 257 | p1_mini_id: p1_mini_1 258 | obis_code: "42.7.0" 259 | name: "Momentary Active Export Phase 2" 260 | icon: "mdi:transmission-tower-import" 261 | unit_of_measurement: kW 262 | accuracy_decimals: 3 263 | device_class: "power" 264 | state_class: "measurement" 265 | - platform: p1_mini 266 | p1_mini_id: p1_mini_1 267 | obis_code: "61.7.0" 268 | name: "Momentary Active Import Phase 3" 269 | icon: "mdi:transmission-tower-export" 270 | unit_of_measurement: kW 271 | accuracy_decimals: 3 272 | device_class: "power" 273 | state_class: "measurement" 274 | - platform: p1_mini 275 | p1_mini_id: p1_mini_1 276 | obis_code: "62.7.0" 277 | name: "Momentary Active Export Phase 3" 278 | icon: "mdi:transmission-tower-import" 279 | unit_of_measurement: kW 280 | accuracy_decimals: 3 281 | device_class: "power" 282 | state_class: "measurement" 283 | - platform: p1_mini 284 | p1_mini_id: p1_mini_1 285 | obis_code: "23.7.0" 286 | name: "Momentary Reactive Import Phase 1" 287 | icon: "mdi:transmission-tower-export" 288 | unit_of_measurement: kvar 289 | accuracy_decimals: 3 290 | - platform: p1_mini 291 | p1_mini_id: p1_mini_1 292 | obis_code: "24.7.0" 293 | name: "Momentary Reactive Export Phase 1" 294 | icon: "mdi:transmission-tower-import" 295 | unit_of_measurement: kvar 296 | accuracy_decimals: 3 297 | - platform: p1_mini 298 | p1_mini_id: p1_mini_1 299 | obis_code: "43.7.0" 300 | name: "Momentary Reactive Import Phase 2" 301 | icon: "mdi:transmission-tower-export" 302 | unit_of_measurement: kvar 303 | accuracy_decimals: 3 304 | - platform: p1_mini 305 | p1_mini_id: p1_mini_1 306 | obis_code: "44.7.0" 307 | name: "Momentary Reactive Export Phase 2" 308 | icon: "mdi:transmission-tower-import" 309 | unit_of_measurement: kvar 310 | accuracy_decimals: 3 311 | - platform: p1_mini 312 | p1_mini_id: p1_mini_1 313 | obis_code: "63.7.0" 314 | name: "Momentary Reactive Import Phase 3" 315 | icon: "mdi:transmission-tower-export" 316 | unit_of_measurement: kvar 317 | accuracy_decimals: 3 318 | - platform: p1_mini 319 | p1_mini_id: p1_mini_1 320 | obis_code: "64.7.0" 321 | name: "Momentary Reactive Export Phase 3" 322 | icon: "mdi:transmission-tower-import" 323 | unit_of_measurement: kvar 324 | accuracy_decimals: 3 325 | - platform: p1_mini 326 | p1_mini_id: p1_mini_1 327 | obis_code: "32.7.0" 328 | name: "Voltage Phase 1" 329 | icon: "mdi:lightning-bolt-outline" 330 | unit_of_measurement: V 331 | accuracy_decimals: 1 332 | device_class: "voltage" 333 | state_class: "measurement" 334 | - platform: p1_mini 335 | p1_mini_id: p1_mini_1 336 | obis_code: "52.7.0" 337 | name: "Voltage Phase 2" 338 | icon: "mdi:lightning-bolt-outline" 339 | unit_of_measurement: V 340 | accuracy_decimals: 1 341 | device_class: "voltage" 342 | state_class: "measurement" 343 | - platform: p1_mini 344 | p1_mini_id: p1_mini_1 345 | obis_code: "72.7.0" 346 | name: "Voltage Phase 3" 347 | icon: "mdi:lightning-bolt-outline" 348 | unit_of_measurement: V 349 | accuracy_decimals: 1 350 | device_class: "voltage" 351 | state_class: "measurement" 352 | - platform: p1_mini 353 | p1_mini_id: p1_mini_1 354 | obis_code: "31.7.0" 355 | name: "Current Phase 1" 356 | icon: "mdi:lightning-bolt" 357 | unit_of_measurement: A 358 | accuracy_decimals: 1 359 | device_class: "current" 360 | state_class: "measurement" 361 | - platform: p1_mini 362 | p1_mini_id: p1_mini_1 363 | obis_code: "51.7.0" 364 | name: "Current Phase 2" 365 | icon: "mdi:lightning-bolt" 366 | unit_of_measurement: A 367 | accuracy_decimals: 1 368 | device_class: "current" 369 | state_class: "measurement" 370 | - platform: p1_mini 371 | p1_mini_id: p1_mini_1 372 | obis_code: "71.7.0" 373 | name: "Current Phase 3" 374 | icon: "mdi:lightning-bolt" 375 | unit_of_measurement: A 376 | accuracy_decimals: 1 377 | device_class: "current" 378 | state_class: "measurement" 379 | # - platform: p1_mini 380 | # p1_mini_id: p1_mini_2 381 | # obis_code: "1.8.0" 382 | # name: "Cumulative Active Import, meter 2" 383 | # unit_of_measurement: kWh 384 | # accuracy_decimals: 3 385 | # state_class: "total_increasing" 386 | # device_class: "energy" 387 | # ... 388 | --------------------------------------------------------------------------------