├── .clang-format ├── .clang-tidy ├── .gitignore ├── FUNDING.yaml ├── LICENSE ├── README.md ├── components └── obis_d0 │ ├── SmartMeterD0.cpp │ ├── SmartMeterD0.h │ ├── __init__.py │ ├── re.cpp │ ├── re.h │ ├── sensor │ ├── SmartMeterD0Sensor.cpp │ ├── SmartMeterD0Sensor.h │ └── __init__.py │ ├── text_sensor │ ├── SmartMeterD0TextSensor.cpp │ ├── SmartMeterD0TextSensor.h │ └── __init__.py │ └── tiny-regex-c │ ├── LICENSE │ └── README.md └── doc ├── ace_3000_type_260.md ├── easymeter_q3d.md ├── ebz_dd3.md ├── ebz_dd3.png ├── ir_reader.jpg ├── iskra_mt174.md ├── iskra_mt174.png └── logarex.md /.clang-format: -------------------------------------------------------------------------------- 1 | # Don't align consecutive assignments or declarations. 2 | # Example: 3 | # int aaaa = 12; 4 | # int b = 13; 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | 8 | # Use space for indentation instead of tab. 9 | UseTab: Never 10 | 11 | # Indentation handling. 12 | IndentWidth: 4 13 | AccessModifierOffset: -4 14 | IndentCaseLabels: true 15 | NamespaceIndentation: All 16 | # Set to 0 to ensure that constructor initializers are always set to new line. 17 | ColumnLimit: 0 18 | 19 | # empty line handling 20 | MaxEmptyLinesToKeep: 1 21 | KeepEmptyLinesAtTheStartOfBlocks: false 22 | 23 | # Brace Wrapping 24 | BreakBeforeBraces: Allman 25 | 26 | # Pointer and reference alignment style. 27 | PointerAlignment: Left 28 | 29 | # Spacing 30 | SpaceBeforeAssignmentOperators: true 31 | SpaceBeforeParens: ControlStatements 32 | SpaceAfterCStyleCast: false 33 | 34 | # Alignment 35 | AlignAfterOpenBracket: AlwaysBreak 36 | BreakConstructorInitializers: AfterColon 37 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 38 | 39 | # Short statement handling 40 | AllowShortFunctionsOnASingleLine: All 41 | AllowShortIfStatementsOnASingleLine: false 42 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | # for list and description of all clang-tidy10 checks see https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/list.html 3 | 4 | Checks: '-*, 5 | clang-analyzer-core.uninitialized.*, 6 | bugprone-assert-side-effect, 7 | bugprone-bad-signal-to-kill-thread, 8 | bugprone-branch-clone, 9 | bugprone-copy-constructor-init, 10 | bugprone-dangling-handle, 11 | bugprone-dynamic-static-initializers, 12 | bugprone-exception-escape, 13 | bugprone-fold-init-type, 14 | bugprone-forward-declaration-namespace, 15 | bugprone-forwarding-reference-overload, 16 | bugprone-inaccurate-erase, 17 | bugprone-incorrect-roundings, 18 | bugprone-infinite-loop, 19 | bugprone-integer-division, 20 | bugprone-lambda-function-name, 21 | bugprone-macro-parentheses, 22 | bugprone-macro-repeated-side-effects, 23 | bugprone-misplaced-operator-in-strlen-in-alloc, 24 | bugprone-misplaced-pointer-arithmetic-in-alloc, 25 | bugprone-misplaced-widening-cast, 26 | bugprone-move-forwarding-reference, 27 | bugprone-multiple-statement-macro, 28 | bugprone-not-null-terminated-result, 29 | bugprone-parent-virtual-call, 30 | bugprone-posix-return, 31 | bugprone-reserved-identifier, 32 | bugprone-signed-char-misuse, 33 | bugprone-sizeof-container, 34 | bugprone-sizeof-expression, 35 | bugprone-spuriously-wake-up-functions, 36 | bugprone-string-constructor, 37 | bugprone-string-integer-assignment, 38 | bugprone-string-literal-with-embedded-nul, 39 | bugprone-suspicious-enum-usage, 40 | bugprone-suspicious-include, 41 | bugprone-suspicious-memset-usage, 42 | bugprone-suspicious-missing-comma, 43 | bugprone-suspicious-semicolon, 44 | bugprone-suspicious-string-compare, 45 | bugprone-swapped-arguments, 46 | bugprone-terminating-continue, 47 | bugprone-throw-keyword-missing, 48 | bugprone-too-small-loop-variable, 49 | bugprone-undefined-memory-manipulation, 50 | bugprone-undelegated-constructor, 51 | bugprone-unhandled-self-assignment, 52 | bugprone-unused-raii, 53 | bugprone-unused-return-value, 54 | bugprone-use-after-move, 55 | bugprone-virtual-near-miss, 56 | google-build-using-namespace, 57 | google-readability-casting, 58 | misc-misplaced-const, 59 | misc-redundant-expression, 60 | misc-static-assert, 61 | misc-unconventional-assign-operator, 62 | misc-uniqueptr-reset-release, 63 | misc-unused-parameters, 64 | modernize-avoid-bind, 65 | modernize-avoid-c-arrays, 66 | modernize-concat-nested-namespaces, 67 | modernize-deprecated-ios-base-aliases, 68 | modernize-loop-convert, 69 | modernize-make-shared, 70 | modernize-make-unique, 71 | modernize-pass-by-value, 72 | modernize-raw-string-literal, 73 | modernize-redundant-void-arg. 74 | modernize-replace-auto-ptr, 75 | modernize-replace-disallow-copy-and-assign-macro, 76 | modernize-replace-random-shuffle, 77 | modernize-return-braced-init-list, 78 | modernize-shrink-to-fit, 79 | modernize-unary-static-assert, 80 | modernize-use-bool-literals, 81 | modernize-use-default-member-init, 82 | modernize-use-emplace, 83 | modernize-use-equals-default, 84 | modernize-use-equals-delete, 85 | modernize-use-noexcept, 86 | modernize-use-nullptr, 87 | modernize-use-override, 88 | modernize-use-uncaught-exceptions, 89 | modernize-use-using, 90 | performance-*, 91 | readability-braces-around-statements, 92 | readability-const-return-type, 93 | readability-container-size-empty, 94 | readability-convert-member-functions-to-static, 95 | readability-delete-null-pointer, 96 | readability-deleted-default, 97 | readability-else-after-return, 98 | readability-function-size, 99 | readability-identifier-naming, 100 | readability-implicit-bool-conversion, 101 | readability-inconsistent-declaration-parameter-name, 102 | readability-isolate-declaration, 103 | # readability-magic-numbers, 104 | readability-make-member-function-const, 105 | readability-misleading-indentation, 106 | readability-misplaced-array-index, 107 | readability-named-parameter, 108 | readability-non-const-parameter, 109 | readability-redundant-access-specifiers, 110 | readability-redundant-control-flow, 111 | readability-redundant-declaration, 112 | readability-redundant-function-ptr-dereference, 113 | readability-redundant-preprocessor, 114 | readability-redundant-smartptr-get, 115 | readability-redundant-string-cstr, 116 | readability-redundant-string-init, 117 | readability-simplify-boolean-expr, 118 | readability-simplify-subscript-expr, 119 | readability-static-accessed-through-instance, 120 | readability-static-definition-in-anonymous-namespace, 121 | readability-string-compare, 122 | readability-uniqueptr-delete-release, 123 | readability-use-anyofallof' 124 | 125 | #WarningsAsErrors: '*' 126 | 127 | CheckOptions: 128 | - { key: readability-function-size.StatementThreshold, value: 1600 } 129 | - { key: readability-function-size.NestingThreshold, value: 12 } 130 | 131 | - { key: readability-identifier-naming.FunctionCase, value: camelBack } 132 | - { key: readability-identifier-naming.MethodCase, value: camelBack } 133 | - { key: readability-identifier-naming.ClassMethodCase, value: camelBack } 134 | 135 | - { key: readability-identifier-naming.ClassCase, value: CamelCase } 136 | - { key: readability-identifier-naming.StructCase, value: CamelCase } 137 | - { key: readability-identifier-naming.EnumCase, value: CamelCase } 138 | 139 | - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } 140 | - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } 141 | - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } 142 | 143 | - { key: readability-identifier-naming.GlobalVariableCase, value: camelBack } 144 | - { key: readability-identifier-naming.StaticVariableCase, value: camelBack } 145 | 146 | - { key: readability-identifier-naming.MemberCase, value: camelBack } 147 | - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } 148 | - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } 149 | 150 | - { key: readability-identifier-naming.NamespaceCase, value: lower_case } 151 | - { key: readability-identifier-naming.LocalVariableCase, value: camelBack } 152 | - { key: readability-identifier-naming.ParameterCase, value: camelBack } 153 | 154 | - { key: readability-inconsistent-declaration-parameter-name.Strict, value: 1 } 155 | 156 | ... 157 | 158 | # removed checks 159 | #bugprone-argument-comment, 160 | #bugprone-bool-pointer-implicit-conversion, 161 | #modernize-deprecated-headers, 162 | #modernize-use-auto, 163 | #modernize-use-nodiscard, 164 | #modernize-use-trailing-return-type, 165 | #modernize-use-transparent-functors, 166 | #readability-qualified-auto, 167 | #readability-redundant-member-init, 168 | #readability-uppercase-literal-suffix, -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,git,python,clion+all,visualstudiocode,platformio 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,git,python,clion+all,visualstudiocode,platformio 3 | 4 | ### CLion+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # SonarLint plugin 69 | .idea/sonarlint/ 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### CLion+all Patch ### 84 | # Ignore everything but code style settings and run configurations 85 | # that are supposed to be shared within teams. 86 | 87 | .idea/* 88 | 89 | #!.idea/codeStyles 90 | #!.idea/runConfigurations 91 | 92 | ### Git ### 93 | # Created by git for backups. To disable backups in Git: 94 | # $ git config --global mergetool.keepBackup false 95 | *.orig 96 | 97 | # Created by git when using merge tools for conflicts 98 | *.BACKUP.* 99 | *.BASE.* 100 | *.LOCAL.* 101 | *.REMOTE.* 102 | *_BACKUP_*.txt 103 | *_BASE_*.txt 104 | *_LOCAL_*.txt 105 | *_REMOTE_*.txt 106 | 107 | ### Linux ### 108 | *~ 109 | 110 | # temporary files which can be created if a process still has a handle open of a deleted file 111 | .fuse_hidden* 112 | 113 | # KDE directory preferences 114 | .directory 115 | 116 | # Linux trash folder which might appear on any partition or disk 117 | .Trash-* 118 | 119 | # .nfs files are created when an open file is removed but is still being accessed 120 | .nfs* 121 | 122 | ### macOS ### 123 | # General 124 | .DS_Store 125 | .AppleDouble 126 | .LSOverride 127 | 128 | # Icon must end with two \r 129 | Icon 130 | 131 | 132 | # Thumbnails 133 | ._* 134 | 135 | # Files that might appear in the root of a volume 136 | .DocumentRevisions-V100 137 | .fseventsd 138 | .Spotlight-V100 139 | .TemporaryItems 140 | .Trashes 141 | .VolumeIcon.icns 142 | .com.apple.timemachine.donotpresent 143 | 144 | # Directories potentially created on remote AFP share 145 | .AppleDB 146 | .AppleDesktop 147 | Network Trash Folder 148 | Temporary Items 149 | .apdisk 150 | 151 | ### macOS Patch ### 152 | # iCloud generated files 153 | *.icloud 154 | 155 | ### PlatformIO ### 156 | .pioenvs 157 | .piolibdeps 158 | .clang_complete 159 | .gcc-flags.json 160 | .pio 161 | 162 | ### Python ### 163 | # Byte-compiled / optimized / DLL files 164 | __pycache__/ 165 | *.py[cod] 166 | *$py.class 167 | 168 | # C extensions 169 | *.so 170 | 171 | # Distribution / packaging 172 | .Python 173 | build/ 174 | develop-eggs/ 175 | dist/ 176 | downloads/ 177 | eggs/ 178 | .eggs/ 179 | lib/ 180 | lib64/ 181 | parts/ 182 | sdist/ 183 | var/ 184 | wheels/ 185 | share/python-wheels/ 186 | *.egg-info/ 187 | .installed.cfg 188 | *.egg 189 | MANIFEST 190 | 191 | # PyInstaller 192 | # Usually these files are written by a python script from a template 193 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 194 | *.manifest 195 | *.spec 196 | 197 | # Installer logs 198 | pip-log.txt 199 | pip-delete-this-directory.txt 200 | 201 | # Unit test / coverage reports 202 | htmlcov/ 203 | .tox/ 204 | .nox/ 205 | .coverage 206 | .coverage.* 207 | .cache 208 | nosetests.xml 209 | coverage.xml 210 | *.cover 211 | *.py,cover 212 | .hypothesis/ 213 | .pytest_cache/ 214 | cover/ 215 | 216 | # Translations 217 | *.mo 218 | *.pot 219 | 220 | # Django stuff: 221 | *.log 222 | local_settings.py 223 | db.sqlite3 224 | db.sqlite3-journal 225 | 226 | # Flask stuff: 227 | instance/ 228 | .webassets-cache 229 | 230 | # Scrapy stuff: 231 | .scrapy 232 | 233 | # Sphinx documentation 234 | docs/_build/ 235 | 236 | # PyBuilder 237 | .pybuilder/ 238 | target/ 239 | 240 | # Jupyter Notebook 241 | .ipynb_checkpoints 242 | 243 | # IPython 244 | profile_default/ 245 | ipython_config.py 246 | 247 | # pyenv 248 | # For a library or package, you might want to ignore these files since the code is 249 | # intended to run in multiple environments; otherwise, check them in: 250 | # .python-version 251 | 252 | # pipenv 253 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 254 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 255 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 256 | # install all needed dependencies. 257 | #Pipfile.lock 258 | 259 | # poetry 260 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 261 | # This is especially recommended for binary packages to ensure reproducibility, and is more 262 | # commonly ignored for libraries. 263 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 264 | #poetry.lock 265 | 266 | # pdm 267 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 268 | #pdm.lock 269 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 270 | # in version control. 271 | # https://pdm.fming.dev/#use-with-ide 272 | .pdm.toml 273 | 274 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 275 | __pypackages__/ 276 | 277 | # Celery stuff 278 | celerybeat-schedule 279 | celerybeat.pid 280 | 281 | # SageMath parsed files 282 | *.sage.py 283 | 284 | # Environments 285 | .env 286 | .venv 287 | env/ 288 | venv/ 289 | ENV/ 290 | env.bak/ 291 | venv.bak/ 292 | 293 | # Spyder project settings 294 | .spyderproject 295 | .spyproject 296 | 297 | # Rope project settings 298 | .ropeproject 299 | 300 | # mkdocs documentation 301 | /site 302 | 303 | # mypy 304 | .mypy_cache/ 305 | .dmypy.json 306 | dmypy.json 307 | 308 | # Pyre type checker 309 | .pyre/ 310 | 311 | # pytype static type analyzer 312 | .pytype/ 313 | 314 | # Cython debug symbols 315 | cython_debug/ 316 | 317 | # PyCharm 318 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 319 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 320 | # and can be added to the global gitignore or merged into this file. For a more nuclear 321 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 322 | #.idea/ 323 | 324 | ### Python Patch ### 325 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 326 | poetry.toml 327 | 328 | # ruff 329 | .ruff_cache/ 330 | 331 | # LSP config files 332 | pyrightconfig.json 333 | 334 | ### VisualStudioCode ### 335 | .vscode/* 336 | !.vscode/settings.json 337 | !.vscode/tasks.json 338 | !.vscode/launch.json 339 | !.vscode/extensions.json 340 | !.vscode/*.code-snippets 341 | 342 | # Local History for Visual Studio Code 343 | .history/ 344 | 345 | # Built Visual Studio Code Extensions 346 | *.vsix 347 | 348 | ### VisualStudioCode Patch ### 349 | # Ignore all local history of files 350 | .history 351 | .ionide 352 | 353 | ### Windows ### 354 | # Windows thumbnail cache files 355 | Thumbs.db 356 | Thumbs.db:encryptable 357 | ehthumbs.db 358 | ehthumbs_vista.db 359 | 360 | # Dump file 361 | *.stackdump 362 | 363 | # Folder config file 364 | [Dd]esktop.ini 365 | 366 | # Recycle Bin used on file shares 367 | $RECYCLE.BIN/ 368 | 369 | # Windows Installer files 370 | *.cab 371 | *.msi 372 | *.msix 373 | *.msm 374 | *.msp 375 | 376 | # Windows shortcuts 377 | *.lnk 378 | 379 | # End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,git,python,clion+all,visualstudiocode,platformio 380 | -------------------------------------------------------------------------------- /FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: [mampfes] 2 | custom: ["https://www.paypal.me/mampfes"] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steffen Zimmermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPHome Smart Meter OBIS D0 Component 2 | 3 | This repository adds support for smart meters with OBIS D0 interface to [ESPHome](https://esphome.io). 4 | 5 | ## Configuration 6 | 7 | This component can be easily added as an [external component](https://esphome.io/components/external_components.html) to ESPHome. 8 | 9 | Communication with the smart meter is done using UART, so you need to configure the [UART bus](https://esphome.io/components/uart.html#uart). 10 | 11 | ```yaml 12 | external_components: 13 | - source: github://mampfes/esphome_obis_d0 14 | 15 | uart: 16 | id: my_uart 17 | rx_pin: GPIO16 18 | baud_rate: 9600 19 | data_bits: 7 20 | parity: EVEN 21 | stop_bits: 1 22 | 23 | obis_d0: 24 | id: my_sm 25 | uart_id: my_uart 26 | on_telegram: 27 | then: 28 | - logger.log: "telegram received" 29 | 30 | sensor: 31 | - platform: obis_d0 32 | name: "Consumed Energy" 33 | obis_d0_id: my_sm 34 | obis_code: "1-0:1.8.0*255" 35 | unit_of_measurement: kWh 36 | accuracy_decimals: 4 37 | state_class: total_increasing 38 | device_class: energy 39 | value_regex: "\\d{6}\\.\\d{8}\\*kWh" 40 | 41 | text_sensor: 42 | - platform: obis_d0 43 | name: "Device Identification" 44 | obis_d0_id: my_sm 45 | obis_code: "1-0:96.1.0*255" 46 | entity_category: diagnostic 47 | value_regex: "\\w{14}" 48 | 49 | - platform: obis_d0 50 | name: "Manufacturer ID" 51 | obis_d0_id: my_sm 52 | obis_code: "id" 53 | entity_category: diagnostic 54 | value_regex: "\\w+" 55 | ``` 56 | 57 | ## Configuration Variables 58 | 59 | ### OBIS D0 platform 60 | 61 | - **id** (Optional, [ID](https://esphome.io/guides/configuration-types.html#config-id)): Manually specify the ID used for code generation. 62 | - **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types.html#config-id)): ID of the [UART Component](https://esphome.io/components/uart.html#uart) if you want to use multiple UART buses. 63 | 64 | ### Sensor 65 | 66 | - **obis_code** (Required, string): Specify the OBIS code you want to retrieve data for from the device. 67 | - **obis_d0_id** (Optional, [ID](https://esphome.io/guides/configuration-types.html#config-id)): ID of the *OBIS D0* Component if you want to manage multiple smart meters. 68 | - **value_regex** (Optional, string): Regular expression to check the validity of the OBIS value. If received value does't match the regular expression, it will be discarded. 69 | - **format** (Optional, string): Format of the OBIS value. Possible choices: `float`, `hex`. Defaults to `float`. 70 | - **timeout** (Optional, [Time](https://esphome.io/guides/configuration-types.html#config-time)): Timeout after which value will be invalidated. Defaults to `5s`. 71 | - All other options form [Sensor](https://esphome.io/components/sensor/index.html#config-sensor). 72 | 73 | ### Text Sensor 74 | 75 | - **obis_code** (Required, string): Specify the OBIS code you want to retrieve data for from the device. 76 | - All other options from [Text Sensor](https://esphome.io/components/text_sensor/index.html#config-text-sensor). 77 | 78 | **NOTE**: There is one special OBIS code for text sensors: 79 | `id` returns the manufacturer identification at the beginning of an OBIS telegram (see example above). 80 | 81 | ### Automations 82 | 83 | - **on_telegram** (Optional, [Automation](https://esphome.io/guides/automations.html#automation)): An automation to perform after a complete telegram (consisting of manufacturer identification and OBIS records) has been received. 84 | 85 | --- 86 | 87 | ## Hardware 88 | 89 | A infrared reader is required to attach to the smart meter. 90 | 91 | I am using the "IR-Lese-Schreibkopf" from Hicham Belmadani: 92 | 93 | ![Infrared Reader](doc/ir_reader.jpg) 94 | 95 | This device is sold on [ebay](https://www.ebay.de/itm/314015465828). 96 | 97 | **NOTE:** I'm not responsible or anyhow associated with this offering! 98 | 99 | --- 100 | 101 | ## Supported Smart Meters 102 | 103 | This section lists supported smart meters: 104 | 105 | | Device | Status | 106 | |-|-| 107 | | [eBZ DD3 Drehstromzähler](doc/ebz_dd3.md) | Supported | 108 | | [ISKRA MT174](doc/iskra_mt174.md) | Supported | 109 | | [Logarex LK11 / LK13](doc/logarex.md) | Supported | 110 | | [EasyMeter Q3D](doc/easymeter_q3.md) | Supported | 111 | | [ACE 3000 Type 260](doc/ace_3000_type_260.md) | Supported | 112 | 113 | ## Important Notes 114 | 115 | ### Error Detection 116 | 117 | The OBIS D0 data format is a textual format based on ASCII characters without error detection and correction mechanisms except the parity bit. Parity errors are detected by the underlying UART layer, but in case of an error the characters are simply omitted - without further notifications to the application. This leads to corrupt readings being read which completely messes the statistics in Home Assistant. Therefore you can specify the expected format for each using regular expressions. If the received data doesn't match the regular expression (e.g. in case of parity error), the data will be discarded. 118 | 119 | If anyone knows how to get a notification from the UART layer in case of an error - please let me know! 120 | 121 | ### ESP-01 122 | 123 | The default implementation uses to much flash space (>512k) to be used on an ESP-01. If you want to use an ESP-01, this is possible by using a limited implementation of the regex library. You may activate it by adding `optimize_size: true` to the `obis_d0` component. 124 | 125 | ```yaml 126 | 127 | obis_d0: 128 | id: my_sm 129 | uart_id: my_uart 130 | optimize_size: true 131 | 132 | ``` 133 | 134 | This smaller implementation of regex comes with some caveats. They are described in the following section. 135 | 136 | #### RegEx compatibility 137 | 138 | The [tiny-regex-c library](https://github.com/kokke/tiny-regex-c/tree/master) is used to reduce the size of the resulting executable. This comes with some caveats regarding the regex capabilities. 139 | 140 | The library was somewhat extended and should include most required cases. Also error logs were added if the regex contains unsupported features. 141 | 142 | #### Unsupported RegEx Features 143 | - `()` any grouping mechanisms are not supported 144 | - `|` branching is not supported 145 | - Additionally the regex must match the entire value, partial matches are detected as an error. \ 146 | as if they are surrounded with `^` and `$` 147 | -------------------------------------------------------------------------------- /components/obis_d0/SmartMeterD0.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/log.h" 2 | 3 | #include "SmartMeterD0.h" 4 | 5 | #ifdef COMPONENT_OBIS_D0_OPTIMIZE_SIZE 6 | #include "re.h" 7 | #else 8 | #include 9 | #endif 10 | 11 | namespace esphome 12 | { 13 | namespace obis_d0 14 | { 15 | static const char* const TAG = "obis_d0"; 16 | 17 | // STX and ETX are sent by an Iskraemeco MT174 after requesting a telegram. 18 | static constexpr uint8_t STX = 0x02; 19 | static constexpr uint8_t ETX = 0x02; 20 | 21 | #ifdef COMPONENT_OBIS_D0_OPTIMIZE_SIZE 22 | bool SmartMeterD0SensorBase::check_value(const std::string& value) 23 | { 24 | int matchlen = 0; 25 | int targetlen = value.length(); 26 | int begin = re_match(value_regex_.c_str(), value.c_str(), &matchlen); 27 | 28 | const char* parse_error = re_last_error(); 29 | if (parse_error != 0) 30 | { 31 | ESP_LOGE(TAG, 32 | "%s: regex '%s' failed to parse: %s", 33 | obis_code_.c_str(), 34 | value_regex_.c_str(), 35 | parse_error); 36 | return false; 37 | } 38 | 39 | if (begin != 0 || matchlen != targetlen) 40 | { 41 | ESP_LOGW(TAG, 42 | "regex '%s' does not match entire value ('%s' -> '%s') matched %d to %d of length %d: %s", 43 | value_regex_.c_str(), 44 | obis_code_.c_str(), 45 | value.c_str(), 46 | begin, 47 | matchlen, 48 | targetlen); 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | #else 55 | bool SmartMeterD0SensorBase::check_value(const std::string& value) 56 | { 57 | bool ok = std::regex_match(value, value_regex_); 58 | 59 | if (!ok) 60 | { 61 | ESP_LOGW(TAG, "value regex doesn't match: %s -> %s", obis_code_.c_str(), value.c_str()); 62 | } 63 | 64 | return ok; 65 | } 66 | #endif 67 | 68 | void SmartMeterD0SensorBase::reset_timeout_counter() 69 | { 70 | lastUpdate_ = millis(); 71 | } 72 | 73 | bool SmartMeterD0SensorBase::has_timed_out() 74 | { 75 | auto now = millis(); 76 | if (lastUpdate_ != 0 && now - lastUpdate_ > timeout_) 77 | { 78 | lastUpdate_ = 0; // invalidate last update time in case of a timeout 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | SmartMeterD0::SmartMeterD0() 86 | { 87 | reset(); 88 | } 89 | 90 | void SmartMeterD0::register_sensor(ISmartMeterD0Sensor* sensor) 91 | { 92 | sensors_.emplace(sensor->get_obis_code(), sensor); 93 | } 94 | 95 | void SmartMeterD0::reset() 96 | { 97 | search_ = &SmartMeterD0::search_start_of_telegram; 98 | } 99 | 100 | void SmartMeterD0::loop() 101 | { 102 | while (available() > 0) 103 | { 104 | (this->*search_)(); 105 | } 106 | 107 | // check timeout 108 | for (auto& it : sensors_) 109 | { 110 | if (it.second->has_timed_out()) 111 | { 112 | it.second->publish_invalid(); 113 | } 114 | } 115 | } 116 | 117 | void SmartMeterD0::search_start_of_telegram() 118 | { 119 | uint8_t* dest = &buffer_[length_++]; 120 | (void)read_byte(dest); 121 | 122 | // check if this the start of a telegram 123 | if (*dest == '/') 124 | { 125 | // start of telegram detected 126 | search_ = &SmartMeterD0::search_end_of_record; 127 | length_ = 1; 128 | } 129 | } 130 | 131 | void SmartMeterD0::search_end_of_record() 132 | { 133 | uint8_t* dest = &buffer_[length_++]; 134 | (void)read_byte(dest); 135 | 136 | // check if this the end of a record 137 | if (*dest == '\n') 138 | { 139 | parse_record(); 140 | length_ = 0; 141 | } 142 | else if (*dest == STX || *dest == ETX) 143 | { 144 | // ignore character 145 | length_--; 146 | } 147 | 148 | if (length_ == buffer_.size()) 149 | { 150 | // abort before overflow 151 | reset(); 152 | ESP_LOGD(TAG, "abort"); 153 | } 154 | } 155 | 156 | void SmartMeterD0::parse_record() 157 | { 158 | if (length_ < 2) 159 | { 160 | // invalid or empty 161 | ESP_LOGD(TAG, "invalid telegram"); 162 | return; 163 | } 164 | 165 | if (length_ == 2) 166 | { 167 | // empty record 168 | return; 169 | } 170 | 171 | if (buffer_[length_ - 2] != '\r') 172 | { 173 | // invalid end of record 174 | ESP_LOGD(TAG, "invalid end or record"); 175 | return; 176 | } 177 | 178 | switch (buffer_[0]) 179 | { 180 | case '/': 181 | parse_identification(); 182 | break; 183 | 184 | case '!': 185 | end_of_telegram(); 186 | break; 187 | 188 | default: 189 | parse_obis(); 190 | break; 191 | } 192 | } 193 | 194 | void SmartMeterD0::parse_identification() 195 | { 196 | std::string ident(reinterpret_cast(&buffer_[1]), reinterpret_cast(&buffer_[length_ - 2])); 197 | 198 | ESP_LOGD(TAG, "Identification: %s ", ident.c_str()); 199 | 200 | // search sensor 201 | auto it = sensors_.find("id"); 202 | if (it != sensors_.end()) 203 | { 204 | it->second->publish_val(ident); 205 | } 206 | } 207 | 208 | void SmartMeterD0::parse_obis() 209 | { 210 | // example: "1-0:0.0.0*255(1023090014472256)\r\n" 211 | // | code | value | 212 | 213 | if (length_ < 4) 214 | { 215 | ESP_LOGD(TAG, "invalid OBIS length"); 216 | return; 217 | } 218 | 219 | // check end of OBIS value 220 | if (buffer_[length_ - 3] != ')') 221 | { 222 | ESP_LOGD(TAG, "OBIS value end missing"); 223 | return; 224 | } 225 | 226 | buffer_[length_ - 3] = 0; // set null terminator at end of OBIS value ")" 227 | 228 | const char* codeStart = reinterpret_cast(&buffer_[0]); 229 | 230 | // search begin of OBIS value 231 | const char* const valueRecordStart = strchr(codeStart, '('); 232 | if (valueRecordStart == nullptr) 233 | { 234 | ESP_LOGD(TAG, "OBIS value start missing"); 235 | return; 236 | } 237 | 238 | std::string code(reinterpret_cast(&buffer_[0]), valueRecordStart); 239 | std::string value(valueRecordStart + 1, reinterpret_cast(&buffer_[length_] - 3)); 240 | const char* const valueStart = valueRecordStart + 1; 241 | 242 | ESP_LOGD(TAG, "OBIS info: %s -> %s", code.c_str(), value.c_str()); 243 | 244 | // search sensor 245 | auto it = sensors_.find(code); 246 | if (it != sensors_.end()) 247 | { 248 | it->second->publish_val(value); 249 | } 250 | } 251 | 252 | void SmartMeterD0::end_of_telegram() 253 | { 254 | for (auto* trigger : telegramTriggers_) 255 | { 256 | trigger->trigger(); 257 | } 258 | 259 | reset(); 260 | } 261 | 262 | } // namespace obis_d0 263 | } // namespace esphome -------------------------------------------------------------------------------- /components/obis_d0/SmartMeterD0.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #ifndef COMPONENT_OBIS_D0_OPTIMIZE_SIZE 7 | #include 8 | #endif 9 | #include 10 | 11 | #include "esphome/components/uart/uart.h" 12 | #include "esphome/core/automation.h" 13 | #include "esphome/core/component.h" 14 | 15 | namespace esphome 16 | { 17 | namespace obis_d0 18 | { 19 | // input string formats for float/int sensors 20 | enum ValueFormat : uint8_t 21 | { 22 | ValueFormat_Float, 23 | ValueFormat_Hex, 24 | }; 25 | 26 | class ISmartMeterD0Sensor 27 | { 28 | public: 29 | virtual ~ISmartMeterD0Sensor() = default; 30 | 31 | virtual void publish_val(const std::string& value) = 0; 32 | virtual void publish_invalid() = 0; 33 | virtual bool has_timed_out() = 0; 34 | virtual const std::string& get_obis_code() const = 0; 35 | }; 36 | 37 | class SmartMeterD0SensorBase : public ISmartMeterD0Sensor 38 | { 39 | public: 40 | SmartMeterD0SensorBase(std::string obis_code, std::string value_regex, int timeout_ms) : 41 | obis_code_{std::move(obis_code)}, 42 | #ifdef COMPONENT_OBIS_D0_OPTIMIZE_SIZE 43 | value_regex_{std::move(value_regex)}, 44 | #else 45 | value_regex_{value_regex}, 46 | #endif 47 | timeout_{static_cast(timeout_ms)} 48 | { 49 | } 50 | 51 | const std::string& get_obis_code() const override { return obis_code_; } 52 | 53 | protected: 54 | std::string obis_code_; 55 | 56 | bool check_value(const std::string& value); 57 | 58 | void reset_timeout_counter(); 59 | bool has_timed_out() override; 60 | 61 | private: 62 | #ifdef COMPONENT_OBIS_D0_OPTIMIZE_SIZE 63 | std::string value_regex_; 64 | #else 65 | std::regex value_regex_; 66 | #endif 67 | uint32_t lastUpdate_{0}; // in milliseconds 68 | const uint32_t timeout_; // in milliseconds 69 | }; 70 | 71 | class TelegramTrigger : public Trigger<> 72 | { 73 | }; 74 | 75 | class SmartMeterD0 : public Component, 76 | public uart::UARTDevice 77 | { 78 | public: 79 | SmartMeterD0(); 80 | 81 | void setup() override 82 | { 83 | // nothing to do here 84 | } 85 | 86 | void loop() override; 87 | 88 | void register_sensor(ISmartMeterD0Sensor* sensor); 89 | 90 | void register_telegram_trigger(TelegramTrigger* trigger) { telegramTriggers_.push_back(trigger); } 91 | 92 | protected: 93 | void reset(); 94 | 95 | void search_start_of_telegram(); 96 | void search_end_of_record(); 97 | 98 | void parse_record(); 99 | void parse_identification(); 100 | void parse_obis(); 101 | 102 | void end_of_telegram(); 103 | 104 | private: 105 | std::array buffer_; 106 | uint16_t length_{0}; // used bytes in buffer_ 107 | 108 | using SearchFct = void (SmartMeterD0::*)(); 109 | SearchFct search_{nullptr}; 110 | 111 | std::map sensors_; 112 | std::vector telegramTriggers_; 113 | }; 114 | 115 | } // namespace obis_d0 116 | } // namespace esphome 117 | -------------------------------------------------------------------------------- /components/obis_d0/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import esphome.codegen as cg 4 | import esphome.config_validation as cv 5 | from esphome.cpp_helpers import setup_entity 6 | from esphome.components import uart 7 | from esphome.const import CONF_ID, CONF_TRIGGER_ID 8 | from esphome import automation 9 | 10 | CODEOWNERS = ["@mampfes"] 11 | 12 | DEPENDENCIES = ["uart"] 13 | 14 | obis_d0_ns = cg.esphome_ns.namespace("obis_d0") 15 | SmartMeterD0 = obis_d0_ns.class_("SmartMeterD0", cg.Component, uart.UARTDevice) 16 | MULTI_CONF = True 17 | 18 | CONF_OBIS_D0_ID = "obis_d0_id" 19 | CONF_OBIS_CODE = "obis_code" 20 | CONF_VALUE_REGEX = "value_regex" 21 | CONF_ON_TELEGRAM = "on_telegram" 22 | CONF_OPTIMIZE_SIZE = "optimize_size" 23 | 24 | # Triggers 25 | TelegramTrigger = obis_d0_ns.class_("TelegramTrigger", automation.Trigger.template()) 26 | 27 | CONFIG_SCHEMA = cv.Schema( 28 | { 29 | cv.GenerateID(): cv.declare_id(SmartMeterD0), 30 | cv.Optional(CONF_ON_TELEGRAM): automation.validate_automation( 31 | { 32 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TelegramTrigger), 33 | } 34 | ), 35 | cv.Optional(CONF_OPTIMIZE_SIZE, False): cv.boolean 36 | } 37 | ).extend(uart.UART_DEVICE_SCHEMA) 38 | 39 | 40 | async def to_code(config): 41 | var = cg.new_Pvariable(config[CONF_ID]) 42 | await cg.register_component(var, config) 43 | await uart.register_uart_device(var, config) 44 | 45 | for conf in config.get(CONF_ON_TELEGRAM, []): 46 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 47 | cg.add(var.register_telegram_trigger(trigger)) 48 | await automation.build_automation(trigger, [], conf) 49 | 50 | if config[CONF_OPTIMIZE_SIZE]: 51 | cg.add_define("COMPONENT_OBIS_D0_OPTIMIZE_SIZE", "1") 52 | 53 | def obis_code(value): 54 | value = cv.string(value) 55 | #match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) 56 | # if match is None: 57 | # raise cv.Invalid(f"{value} is not a valid OBIS code") 58 | return value 59 | -------------------------------------------------------------------------------- /components/obis_d0/re.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mini regex-module inspired by Rob Pike's regex code described in: 4 | * 5 | * http://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html 6 | * 7 | * 8 | * 9 | * Supports: 10 | * --------- 11 | * '.' Dot, matches any character 12 | * '^' Start anchor, matches beginning of string 13 | * '$' End anchor, matches end of string 14 | * '*' Asterisk, match zero or more (greedy) 15 | * '+' Plus, match one or more (greedy) 16 | * '?' Question, match zero or one (non-greedy) 17 | * '[abc]' Character class, match if one of {'a', 'b', 'c'} 18 | * '[^abc]' Inverted class, match if NOT one of {'a', 'b', 'c'} -- NOTE: feature is currently broken! 19 | * '[a-zA-Z]' Character ranges, the character set of the ranges { a-z | A-Z } 20 | * '\s' Whitespace, \t \f \r \n \v and spaces 21 | * '\S' Non-whitespace 22 | * '\w' Alphanumeric, [a-zA-Z0-9_] 23 | * '\W' Non-alphanumeric 24 | * '\d' Digits, [0-9] 25 | * '\D' Non-digits 26 | * 27 | * 28 | */ 29 | 30 | 31 | #include "esphome/core/log.h" 32 | 33 | #include "re.h" 34 | #include 35 | #include 36 | #include 37 | 38 | using namespace esphome; 39 | 40 | /* Definitions: */ 41 | 42 | #define MAX_REGEXP_OBJECTS 30 /* Max number of regex symbols in expression. */ 43 | #define MAX_CHAR_CLASS_LEN 100 /* Max length of character-class buffer in. */ 44 | 45 | enum 46 | { 47 | UNUSED, 48 | DOT, 49 | BEGIN, 50 | END, 51 | QUESTIONMARK, 52 | STAR, 53 | PLUS, 54 | CHAR, 55 | CHAR_CLASS, 56 | INV_CHAR_CLASS, 57 | DIGIT, 58 | NOT_DIGIT, 59 | ALPHA, 60 | NOT_ALPHA, 61 | WHITESPACE, 62 | NOT_WHITESPACE, 63 | BRANCH, 64 | REPEATED, 65 | }; 66 | 67 | typedef struct regex_t 68 | { 69 | unsigned char type; /* CHAR, STAR, etc. */ 70 | union 71 | { 72 | struct 73 | { 74 | unsigned char min; 75 | unsigned char max; 76 | } rep; 77 | unsigned char ch; /* the character itself */ 78 | unsigned char* ccl; /* OR a pointer to characters in class */ 79 | } u; 80 | } regex_t; 81 | 82 | /* Private function declarations: */ 83 | static int matchpattern(regex_t* pattern, const char* text, int* matchlength); 84 | static int matchcharclass(char c, const char* str); 85 | static int matchstar(regex_t p, regex_t* pattern, const char* text, int* matchlength); 86 | static int matchplus(regex_t p, regex_t* pattern, const char* text, int* matchlength); 87 | static int matchrepeated(regex_t p, 88 | regex_t* pattern, 89 | const unsigned char min, 90 | const unsigned char max, 91 | const char* text, 92 | int* matchlength); 93 | static int matchone(regex_t p, char c); 94 | static int matchdigit(char c); 95 | static int matchalpha(char c); 96 | static int matchwhitespace(char c); 97 | static int matchmetachar(char c, const char* str); 98 | static int matchrange(char c, const char* str); 99 | static int matchdot(char c); 100 | static int ismetachar(char c); 101 | 102 | static char re_error_buf[50]; 103 | 104 | /* Public functions: */ 105 | int re_match(const char* pattern, const char* text, int* matchlength) 106 | { 107 | return re_matchp(re_compile(pattern), text, matchlength); 108 | } 109 | 110 | int re_matchp(re_t pattern, const char* text, int* matchlength) 111 | { 112 | *matchlength = 0; 113 | if (pattern != 0) 114 | { 115 | if (pattern[0].type == BEGIN) 116 | { 117 | return ((matchpattern(&pattern[1], text, matchlength)) ? 0 : -1); 118 | } 119 | else 120 | { 121 | int idx = -1; 122 | 123 | do 124 | { 125 | idx += 1; 126 | 127 | if (matchpattern(pattern, text, matchlength)) 128 | { 129 | if (text[0] == '\0') 130 | return -1; 131 | 132 | return idx; 133 | } 134 | 135 | // Reset match length for the next starting point 136 | *matchlength = 0; 137 | 138 | } while (*text++ != '\0'); 139 | } 140 | } 141 | return -1; 142 | } 143 | 144 | re_t re_compile(const char* pattern) 145 | { 146 | /* The sizes of the two static arrays below substantiates the static RAM usage of this module. 147 | MAX_REGEXP_OBJECTS is the max number of symbols in the expression. 148 | MAX_CHAR_CLASS_LEN determines the size of buffer for chars in all char-classes in the expression. */ 149 | static regex_t re_compiled[MAX_REGEXP_OBJECTS]; 150 | static unsigned char ccl_buf[MAX_CHAR_CLASS_LEN]; 151 | int ccl_bufidx = 1, ccl_buftemp = 1; 152 | 153 | char c; /* current char in pattern */ 154 | int i = 0; /* index into pattern */ 155 | int j = 0; /* index into re_compiled */ 156 | 157 | /* reset error buf */ 158 | re_error_buf[0] = 0; 159 | 160 | while (pattern[i] != '\0' && (j + 1 < MAX_REGEXP_OBJECTS)) 161 | { 162 | c = pattern[i]; 163 | 164 | switch (c) 165 | { 166 | /* Meta-characters: */ 167 | case '^': 168 | { 169 | re_compiled[j].type = BEGIN; 170 | } 171 | break; 172 | case '$': 173 | { 174 | re_compiled[j].type = END; 175 | } 176 | break; 177 | case '.': 178 | { 179 | re_compiled[j].type = DOT; 180 | } 181 | break; 182 | case '*': 183 | { 184 | re_compiled[j].type = STAR; 185 | } 186 | break; 187 | case '+': 188 | { 189 | re_compiled[j].type = PLUS; 190 | } 191 | break; 192 | case '?': 193 | { 194 | re_compiled[j].type = QUESTIONMARK; 195 | } 196 | break; 197 | case '|': 198 | { 199 | re_compiled[j].type = BRANCH; 200 | sprintf(re_error_buf, "branching currently not supported"); 201 | return 0; 202 | } 203 | break; 204 | 205 | case '(': 206 | { 207 | sprintf(re_error_buf, "groups not supported"); 208 | return 0; 209 | } 210 | break; 211 | 212 | /* Escaped character-classes (\s \w ...): */ 213 | case '\\': 214 | { 215 | if (pattern[i + 1] != '\0') 216 | { 217 | /* Skip the escape-char '\\' */ 218 | i += 1; 219 | /* ... and check the next */ 220 | switch (pattern[i]) 221 | { 222 | /* Meta-character: */ 223 | case 'd': 224 | { 225 | re_compiled[j].type = DIGIT; 226 | } 227 | break; 228 | case 'D': 229 | { 230 | re_compiled[j].type = NOT_DIGIT; 231 | } 232 | break; 233 | case 'w': 234 | { 235 | re_compiled[j].type = ALPHA; 236 | } 237 | break; 238 | case 'W': 239 | { 240 | re_compiled[j].type = NOT_ALPHA; 241 | } 242 | break; 243 | case 's': 244 | { 245 | re_compiled[j].type = WHITESPACE; 246 | } 247 | break; 248 | case 'S': 249 | { 250 | re_compiled[j].type = NOT_WHITESPACE; 251 | } 252 | break; 253 | 254 | /* Escaped character, e.g. '.' or '$' */ 255 | default: 256 | { 257 | re_compiled[j].type = CHAR; 258 | re_compiled[j].u.ch = pattern[i]; 259 | } 260 | break; 261 | } 262 | } 263 | else 264 | { 265 | sprintf(re_error_buf, "regex ends with \\"); 266 | return 0; 267 | } 268 | } 269 | break; 270 | 271 | /* Character class: */ 272 | case '[': 273 | { 274 | /* Remember where the char-buffer starts. */ 275 | int buf_begin = ccl_bufidx; 276 | 277 | /* Look-ahead to determine if negated */ 278 | if (pattern[i + 1] == '^') 279 | { 280 | re_compiled[j].type = INV_CHAR_CLASS; 281 | i += 1; /* Increment i to avoid including '^' in the char-buffer */ 282 | if (pattern[i + 1] == 0) /* incomplete pattern, missing non-zero char after '^' */ 283 | { 284 | return 0; 285 | } 286 | } 287 | else 288 | { 289 | re_compiled[j].type = CHAR_CLASS; 290 | } 291 | 292 | /* Copy characters inside [..] to buffer */ 293 | while ((pattern[++i] != ']') 294 | && (pattern[i] != '\0')) /* Missing ] */ 295 | { 296 | if (pattern[i] == '\\') 297 | { 298 | if (ccl_bufidx >= MAX_CHAR_CLASS_LEN - 1) 299 | { 300 | sprintf(re_error_buf, "exceeded internal buffer!"); 301 | return 0; 302 | } 303 | if (pattern[i + 1] == 0) /* incomplete pattern, missing non-zero char after '\\' */ 304 | { 305 | return 0; 306 | } 307 | ccl_buf[ccl_bufidx++] = pattern[i++]; 308 | } 309 | else if (ccl_bufidx >= MAX_CHAR_CLASS_LEN) 310 | { 311 | //fputs("exceeded internal buffer!\n", stderr); 312 | return 0; 313 | } 314 | ccl_buf[ccl_bufidx++] = pattern[i]; 315 | } 316 | if (ccl_bufidx >= MAX_CHAR_CLASS_LEN) 317 | { 318 | /* Catches cases such as [00000000000000000000000000000000000000][ */ 319 | sprintf(re_error_buf, "exceeded internal buffer!"); 320 | return 0; 321 | } 322 | /* Null-terminate string end */ 323 | ccl_buf[ccl_bufidx++] = 0; 324 | re_compiled[j].u.ccl = &ccl_buf[buf_begin]; 325 | } 326 | break; 327 | 328 | case '{': 329 | { 330 | re_compiled[j].type = REPEATED; 331 | re_compiled[j].u.rep.min = 255; 332 | ccl_buftemp = ccl_bufidx; 333 | 334 | while ((pattern[++i] != '}') 335 | && (pattern[i] != '\0')) /* Missing } */ 336 | { 337 | if (pattern[i] == ',') 338 | { 339 | ccl_buf[ccl_bufidx] = 0; 340 | re_compiled[j].u.rep.min = (char)atoi((const char*)&ccl_buf[ccl_buftemp]); 341 | /* reset buffer for upper limit */ 342 | ccl_bufidx = ccl_buftemp; 343 | } 344 | else if (isdigit(pattern[i])) 345 | { 346 | if (ccl_bufidx >= MAX_CHAR_CLASS_LEN - 1) 347 | { 348 | sprintf(re_error_buf, "exceeded internal buffer!"); 349 | return 0; 350 | } 351 | ccl_buf[ccl_bufidx++] = pattern[i]; 352 | } 353 | else 354 | { 355 | /* Catches {0AZ} */ 356 | sprintf(re_error_buf, "non number digits in repetition expr"); 357 | return 0; 358 | } 359 | } 360 | /* Null-terminate string end */ 361 | ccl_buf[ccl_bufidx] = 0; 362 | re_compiled[j].u.rep.max = (char)atoi((const char*)&ccl_buf[ccl_buftemp]); 363 | if (re_compiled[j].u.rep.min > re_compiled[j].u.rep.max) 364 | { 365 | re_compiled[j].u.rep.min = re_compiled[j].u.rep.max; 366 | } 367 | /* reset buffer */ 368 | ccl_bufidx = ccl_buftemp; 369 | } 370 | break; 371 | 372 | /* Other characters: */ 373 | default: 374 | { 375 | re_compiled[j].type = CHAR; 376 | re_compiled[j].u.ch = c; 377 | } 378 | break; 379 | } 380 | /* no buffer-out-of-bounds access on invalid patterns - see https://github.com/kokke/tiny-regex-c/commit/1a279e04014b70b0695fba559a7c05d55e6ee90b */ 381 | if (pattern[i] == 0) 382 | { 383 | return 0; 384 | } 385 | 386 | i += 1; 387 | j += 1; 388 | } 389 | /* 'UNUSED' is a sentinel used to indicate end-of-pattern */ 390 | re_compiled[j].type = UNUSED; 391 | 392 | return (re_t)re_compiled; 393 | } 394 | 395 | const char* re_last_error() 396 | { 397 | if (re_error_buf[0] == 0) 398 | return 0; 399 | return re_error_buf; 400 | } 401 | 402 | void re_print(const char* TAG, regex_t* pattern) 403 | { 404 | const char* types[] = 405 | {"UNUSED", "DOT", "BEGIN", "END", "QUESTIONMARK", "STAR", "PLUS", "CHAR", "CHAR_CLASS", "INV_CHAR_CLASS", 406 | "DIGIT", 407 | "NOT_DIGIT", "ALPHA", "NOT_ALPHA", "WHITESPACE", "NOT_WHITESPACE", "BRANCH"}; 408 | 409 | int i; 410 | int j; 411 | char c; 412 | for (i = 0; i < MAX_REGEXP_OBJECTS; ++i) 413 | { 414 | if (pattern[i].type == UNUSED) 415 | { 416 | break; 417 | } 418 | 419 | ESP_LOGCONFIG(TAG, "type: %s", types[pattern[i].type]); 420 | if (pattern[i].type == CHAR_CLASS || pattern[i].type == INV_CHAR_CLASS) 421 | { 422 | ESP_LOGCONFIG(TAG, " ["); 423 | for (j = 0; j < MAX_CHAR_CLASS_LEN; ++j) 424 | { 425 | c = pattern[i].u.ccl[j]; 426 | if ((c == '\0') || (c == ']')) 427 | { 428 | break; 429 | } 430 | ESP_LOGCONFIG(TAG, "%c", c); 431 | } 432 | ESP_LOGCONFIG(TAG, "]"); 433 | } 434 | else if (pattern[i].type == CHAR) 435 | { 436 | ESP_LOGCONFIG(TAG, " '%c'", pattern[i].u.ch); 437 | } 438 | printf("\n"); 439 | } 440 | } 441 | 442 | /* Private functions: */ 443 | static int matchdigit(char c) 444 | { 445 | return isdigit(c); 446 | } 447 | static int matchalpha(char c) 448 | { 449 | return isalpha(c); 450 | } 451 | static int matchwhitespace(char c) 452 | { 453 | return isspace(c); 454 | } 455 | static int matchalphanum(char c) 456 | { 457 | return ((c == '_') || matchalpha(c) || matchdigit(c)); 458 | } 459 | static int matchrange(char c, const char* str) 460 | { 461 | return ((c != '-') 462 | && (str[0] != '\0') 463 | && (str[0] != '-') 464 | && (str[1] == '-') 465 | && (str[2] != '\0') 466 | && ((c >= str[0]) 467 | && (c <= str[2]))); 468 | } 469 | static int matchdot(char c) 470 | { 471 | #if defined(RE_DOT_MATCHES_NEWLINE) && (RE_DOT_MATCHES_NEWLINE == 1) 472 | (void)c; 473 | return 1; 474 | #else 475 | return c != '\n' && c != '\r'; 476 | #endif 477 | } 478 | static int ismetachar(char c) 479 | { 480 | return ((c == 's') || (c == 'S') || (c == 'w') || (c == 'W') || (c == 'd') || (c == 'D')); 481 | } 482 | 483 | static int matchmetachar(char c, const char* str) 484 | { 485 | switch (str[0]) 486 | { 487 | case 'd': 488 | return matchdigit(c); 489 | case 'D': 490 | return !matchdigit(c); 491 | case 'w': 492 | return matchalphanum(c); 493 | case 'W': 494 | return !matchalphanum(c); 495 | case 's': 496 | return matchwhitespace(c); 497 | case 'S': 498 | return !matchwhitespace(c); 499 | default: 500 | return (c == str[0]); 501 | } 502 | } 503 | 504 | static int matchcharclass(char c, const char* str) 505 | { 506 | do 507 | { 508 | if (matchrange(c, str)) 509 | { 510 | return 1; 511 | } 512 | else if (str[0] == '\\') 513 | { 514 | /* Escape-char: increment str-ptr and match on next char */ 515 | str += 1; 516 | if (matchmetachar(c, str)) 517 | { 518 | return 1; 519 | } 520 | else if ((c == str[0]) && !ismetachar(c)) 521 | { 522 | return 1; 523 | } 524 | } 525 | else if (c == str[0]) 526 | { 527 | if (c == '-') 528 | { 529 | return ((str[-1] == '\0') || (str[1] == '\0')); 530 | } 531 | else 532 | { 533 | return 1; 534 | } 535 | } 536 | } while (*str++ != '\0'); 537 | 538 | return 0; 539 | } 540 | 541 | static int matchone(regex_t p, char c) 542 | { 543 | switch (p.type) 544 | { 545 | case DOT: 546 | return matchdot(c); 547 | case CHAR_CLASS: 548 | return matchcharclass(c, (const char*)p.u.ccl); 549 | case INV_CHAR_CLASS: 550 | return !matchcharclass(c, (const char*)p.u.ccl); 551 | case DIGIT: 552 | return matchdigit(c); 553 | case NOT_DIGIT: 554 | return !matchdigit(c); 555 | case ALPHA: 556 | return matchalphanum(c); 557 | case NOT_ALPHA: 558 | return !matchalphanum(c); 559 | case WHITESPACE: 560 | return matchwhitespace(c); 561 | case NOT_WHITESPACE: 562 | return !matchwhitespace(c); 563 | default: 564 | return (p.u.ch == c); 565 | } 566 | } 567 | 568 | static int matchstar(regex_t p, regex_t* pattern, const char* text, int* matchlength) 569 | { 570 | int prelen = *matchlength; 571 | const char* prepoint = text; 572 | while ((text[0] != '\0') && matchone(p, *text)) 573 | { 574 | text++; 575 | (*matchlength)++; 576 | } 577 | while (text >= prepoint) 578 | { 579 | if (matchpattern(pattern, text--, matchlength)) 580 | return 1; 581 | (*matchlength)--; 582 | } 583 | 584 | *matchlength = prelen; 585 | return 0; 586 | } 587 | 588 | static int matchplus(regex_t p, regex_t* pattern, const char* text, int* matchlength) 589 | { 590 | const char* prepoint = text; 591 | while ((text[0] != '\0') && matchone(p, *text)) 592 | { 593 | text++; 594 | (*matchlength)++; 595 | } 596 | while (text > prepoint) 597 | { 598 | if (matchpattern(pattern, text--, matchlength)) 599 | return 1; 600 | (*matchlength)--; 601 | } 602 | 603 | return 0; 604 | } 605 | 606 | static int matchrepeated(regex_t p, 607 | regex_t* pattern, 608 | const unsigned char min, 609 | const unsigned char max, 610 | const char* text, 611 | int* matchlength) 612 | { 613 | unsigned char matched = 0; 614 | while ((text[0] != '\0') && matchone(p, *text)) 615 | { 616 | matched++; 617 | text++; 618 | (*matchlength)++; 619 | if (matched >= max) 620 | break; 621 | } 622 | if (matched < min) 623 | return 0; 624 | 625 | do 626 | { 627 | if (matchpattern(pattern, text--, matchlength)) 628 | return 1; 629 | matched--; 630 | (*matchlength)--; 631 | } while (matched > min); 632 | 633 | return 0; 634 | } 635 | 636 | static int matchquestion(regex_t p, regex_t* pattern, const char* text, int* matchlength) 637 | { 638 | if (p.type == UNUSED) 639 | return 1; 640 | if (matchpattern(pattern, text, matchlength)) 641 | return 1; 642 | if (*text && matchone(p, *text++)) 643 | { 644 | if (matchpattern(pattern, text, matchlength)) 645 | { 646 | (*matchlength)++; 647 | return 1; 648 | } 649 | } 650 | return 0; 651 | } 652 | 653 | #if 0 654 | 655 | /* Recursive matching */ 656 | static int matchpattern(regex_t* pattern, const char* text, int *matchlength) 657 | { 658 | int pre = *matchlength; 659 | if ((pattern[0].type == UNUSED) || (pattern[1].type == QUESTIONMARK)) 660 | { 661 | return matchquestion(pattern[1], &pattern[2], text, matchlength); 662 | } 663 | else if (pattern[1].type == STAR) 664 | { 665 | return matchstar(pattern[0], &pattern[2], text, matchlength); 666 | } 667 | else if (pattern[1].type == PLUS) 668 | { 669 | return matchplus(pattern[0], &pattern[2], text, matchlength); 670 | } 671 | else if ((pattern[0].type == END) && pattern[1].type == UNUSED) 672 | { 673 | return text[0] == '\0'; 674 | } 675 | else if ((text[0] != '\0') && matchone(pattern[0], text[0])) 676 | { 677 | (*matchlength)++; 678 | return matchpattern(&pattern[1], text+1); 679 | } 680 | else 681 | { 682 | *matchlength = pre; 683 | return 0; 684 | } 685 | } 686 | 687 | #else 688 | 689 | /* Iterative matching */ 690 | static int matchpattern(regex_t* pattern, const char* text, int* matchlength) 691 | { 692 | int pre = *matchlength; 693 | do 694 | { 695 | if ((pattern[0].type == UNUSED) || (pattern[1].type == QUESTIONMARK)) 696 | { 697 | return matchquestion(pattern[0], &pattern[2], text, matchlength); 698 | } 699 | else if (pattern[1].type == STAR) 700 | { 701 | return matchstar(pattern[0], &pattern[2], text, matchlength); 702 | } 703 | else if (pattern[1].type == PLUS) 704 | { 705 | return matchplus(pattern[0], &pattern[2], text, matchlength); 706 | } 707 | else if (pattern[1].type == REPEATED) 708 | { 709 | return matchrepeated(pattern[0], 710 | &pattern[2], 711 | pattern[1].u.rep.min, 712 | pattern[1].u.rep.max, 713 | text, 714 | matchlength); 715 | } 716 | else if ((pattern[0].type == END) && pattern[1].type == UNUSED) 717 | { 718 | return (text[0] == '\0'); 719 | } 720 | /* Branching is not working properly 721 | else if (pattern[1].type == BRANCH) 722 | { 723 | return (matchpattern(pattern, text) || matchpattern(&pattern[2], text)); 724 | } 725 | */ 726 | (*matchlength)++; 727 | } while ((text[0] != '\0') && matchone(*pattern++, *text++)); 728 | 729 | *matchlength = pre; 730 | return 0; 731 | } 732 | 733 | #endif 734 | -------------------------------------------------------------------------------- /components/obis_d0/re.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mini regex-module inspired by Rob Pike's regex code described in: 4 | * 5 | * http://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html 6 | * 7 | * 8 | * 9 | * Supports: 10 | * --------- 11 | * '.' Dot, matches any character 12 | * '^' Start anchor, matches beginning of string 13 | * '$' End anchor, matches end of string 14 | * '*' Asterisk, match zero or more (greedy) 15 | * '+' Plus, match one or more (greedy) 16 | * '?' Question, match zero or one (non-greedy) 17 | * '[abc]' Character class, match if one of {'a', 'b', 'c'} 18 | * '[^abc]' Inverted class, match if NOT one of {'a', 'b', 'c'} -- NOTE: feature is currently broken! 19 | * '[a-zA-Z]' Character ranges, the character set of the ranges { a-z | A-Z } 20 | * '\s' Whitespace, \t \f \r \n \v and spaces 21 | * '\S' Non-whitespace 22 | * '\w' Alphanumeric, [a-zA-Z0-9_] 23 | * '\W' Non-alphanumeric 24 | * '\d' Digits, [0-9] 25 | * '\D' Non-digits 26 | * 27 | * 28 | */ 29 | 30 | #ifndef _TINY_REGEX_C 31 | #define _TINY_REGEX_C 32 | 33 | #ifndef RE_DOT_MATCHES_NEWLINE 34 | /* Define to 0 if you DON'T want '.' to match '\r' + '\n' */ 35 | #define RE_DOT_MATCHES_NEWLINE 1 36 | #endif 37 | 38 | #ifdef __cplusplus 39 | extern "C"{ 40 | #endif 41 | 42 | /* Typedef'd pointer to get abstract datatype. */ 43 | typedef struct regex_t* re_t; 44 | 45 | /* Compile regex string pattern to a regex_t-array. */ 46 | re_t re_compile(const char* pattern); 47 | 48 | /* Returns the last error message if re_compile failed otherwise nullptr */ 49 | const char* re_last_error(); 50 | 51 | /* Find matches of the compiled pattern inside text. */ 52 | int re_matchp(re_t pattern, const char* text, int* matchlength); 53 | 54 | /* Find matches of the txt pattern inside text (will compile automatically first). */ 55 | int re_match(const char* pattern, const char* text, int* matchlength); 56 | 57 | #ifdef __cplusplus 58 | } 59 | #endif 60 | 61 | #endif /* ifndef _TINY_REGEX_C */ 62 | -------------------------------------------------------------------------------- /components/obis_d0/sensor/SmartMeterD0Sensor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "esphome/core/log.h" 6 | 7 | #include "../SmartMeterD0.h" 8 | #include "SmartMeterD0Sensor.h" 9 | 10 | namespace esphome 11 | { 12 | namespace obis_d0 13 | { 14 | static const char* const TAG = "obis_d0_sensor"; 15 | 16 | SmartMeterD0Sensor::SmartMeterD0Sensor(std::string obis_code, std::string value_regex, ValueFormat format, int timeout_ms) : 17 | SmartMeterD0SensorBase{obis_code, value_regex, timeout_ms} 18 | { 19 | switch (format) 20 | { 21 | case ValueFormat_Float: 22 | publish_ = &SmartMeterD0Sensor::publish_float; 23 | break; 24 | 25 | case ValueFormat_Hex: 26 | publish_ = &SmartMeterD0Sensor::publish_hex; 27 | break; 28 | } 29 | } 30 | 31 | void SmartMeterD0Sensor::publish_val(const std::string& value) 32 | { 33 | if (check_value(value)) 34 | { 35 | (this->*publish_)(value); 36 | reset_timeout_counter(); 37 | } 38 | else 39 | { 40 | publish_invalid(); 41 | } 42 | } 43 | 44 | void SmartMeterD0Sensor::publish_invalid() 45 | { 46 | publish_state(NAN); 47 | } 48 | 49 | void SmartMeterD0Sensor::publish_float(const std::string& value) 50 | { 51 | char* end; 52 | double val = strtod(value.c_str(), &end); 53 | if (val == HUGE_VAL) 54 | { 55 | ESP_LOGD(TAG, "OBIS code %s float out of range: %s", obis_code_.c_str(), value.c_str()); 56 | return; 57 | } 58 | 59 | if (end == value.c_str()) 60 | { 61 | ESP_LOGD(TAG, "OBIS code %s invalid float: %s", obis_code_.c_str(), value.c_str()); 62 | return; 63 | } 64 | 65 | publish_state(val); 66 | } 67 | 68 | void SmartMeterD0Sensor::publish_hex(const std::string& value) 69 | { 70 | char* end; 71 | unsigned long long val = strtoull(value.c_str(), &end, 16); 72 | if (val == HUGE_VAL) 73 | { 74 | ESP_LOGD(TAG, "OBIS code %s hex out of range: %s", obis_code_.c_str(), value.c_str()); 75 | return; 76 | } 77 | 78 | if (end == value.c_str()) 79 | { 80 | ESP_LOGD(TAG, "OBIS code %s invalid hex: %s", obis_code_.c_str(), value.c_str()); 81 | return; 82 | } 83 | 84 | publish_state(val); 85 | } 86 | } // namespace obis_d0 87 | } // namespace esphome -------------------------------------------------------------------------------- /components/obis_d0/sensor/SmartMeterD0Sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/components/sensor/sensor.h" 6 | 7 | #include "../SmartMeterD0.h" 8 | 9 | namespace esphome 10 | { 11 | namespace obis_d0 12 | { 13 | class SmartMeterD0Sensor : public SmartMeterD0SensorBase, public sensor::Sensor, public Component 14 | { 15 | public: 16 | SmartMeterD0Sensor(std::string obis_code, std::string value_regex, ValueFormat format, int timeoutMS); 17 | void publish_val(const std::string& value) override; 18 | void publish_invalid() override; 19 | 20 | protected: 21 | void publish_float(const std::string& value); 22 | void publish_hex(const std::string& value); 23 | 24 | private: 25 | using PublishFct = void (SmartMeterD0Sensor::*)(const std::string&); 26 | PublishFct publish_{nullptr}; 27 | }; 28 | 29 | } // namespace obis_d0 30 | } // namespace esphome 31 | -------------------------------------------------------------------------------- /components/obis_d0/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_OBIS_D0_ID, CONF_OBIS_CODE, CONF_VALUE_REGEX, SmartMeterD0, obis_code, obis_d0_ns 7 | 8 | AUTO_LOAD = ["obis_d0"] 9 | 10 | ValueFormat = obis_d0_ns.enum("ValueFormat") 11 | VALUE_FORMAT_TYPES = { 12 | "float": ValueFormat.ValueFormat_Float, 13 | "hex": ValueFormat.ValueFormat_Hex, 14 | } 15 | 16 | SmartMeterD0Sensor = obis_d0_ns.class_( 17 | "SmartMeterD0Sensor", sensor.Sensor, cg.Component) 18 | 19 | 20 | CONFIG_SCHEMA = sensor.sensor_schema().extend( 21 | { 22 | cv.GenerateID(): cv.declare_id(SmartMeterD0Sensor), 23 | cv.GenerateID(CONF_OBIS_D0_ID): cv.use_id(SmartMeterD0), 24 | cv.Required(CONF_OBIS_CODE): obis_code, 25 | cv.Optional(CONF_VALUE_REGEX, default=".*"): cv.string, 26 | cv.Optional(CONF_FORMAT, default="float"): cv.enum(VALUE_FORMAT_TYPES, lower=True), 27 | cv.Optional(CONF_TIMEOUT, default="5s"): cv.time_period, 28 | } 29 | ) 30 | 31 | 32 | async def to_code(config): 33 | var = cg.new_Pvariable( 34 | config[CONF_ID], 35 | config[CONF_OBIS_CODE], 36 | config[CONF_VALUE_REGEX], 37 | config[CONF_FORMAT], 38 | config[CONF_TIMEOUT].total_milliseconds, 39 | ) 40 | await cg.register_component(var, config) 41 | await sensor.register_sensor(var, config) 42 | obis_d0 = await cg.get_variable(config[CONF_OBIS_D0_ID]) 43 | cg.add(obis_d0.register_sensor(var)) 44 | -------------------------------------------------------------------------------- /components/obis_d0/text_sensor/SmartMeterD0TextSensor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "esphome/core/log.h" 5 | 6 | #include "../SmartMeterD0.h" 7 | #include "SmartMeterD0TextSensor.h" 8 | 9 | namespace esphome 10 | { 11 | namespace obis_d0 12 | { 13 | static const char* const TAG = "obis_d0_text_sensor"; 14 | 15 | SmartMeterD0TextSensor::SmartMeterD0TextSensor(std::string obis_code, std::string value_regex, int timeout_ms) : 16 | SmartMeterD0SensorBase{obis_code, value_regex, timeout_ms} 17 | { 18 | } 19 | 20 | void SmartMeterD0TextSensor::publish_val(const std::string& value) 21 | { 22 | if (check_value(value)) 23 | { 24 | publish_state(value); 25 | reset_timeout_counter(); 26 | } 27 | else 28 | { 29 | publish_invalid(); 30 | } 31 | } 32 | 33 | void SmartMeterD0TextSensor::publish_invalid() 34 | { 35 | publish_state(""); 36 | } 37 | } // namespace obis_d0 38 | } // namespace esphome -------------------------------------------------------------------------------- /components/obis_d0/text_sensor/SmartMeterD0TextSensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/components/text_sensor/text_sensor.h" 6 | 7 | #include "../SmartMeterD0.h" 8 | 9 | namespace esphome 10 | { 11 | namespace obis_d0 12 | { 13 | class SmartMeterD0TextSensor : public SmartMeterD0SensorBase, public text_sensor::TextSensor, public Component 14 | { 15 | public: 16 | SmartMeterD0TextSensor(std::string obis_code, std::string value_regex, int timeout_ms); 17 | void publish_val(const std::string& value) override; 18 | void publish_invalid() override; 19 | 20 | private: 21 | }; 22 | 23 | } // namespace obis_d0 24 | } // namespace esphome 25 | -------------------------------------------------------------------------------- /components/obis_d0/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_ID, CONF_TIMEOUT 5 | 6 | from .. import CONF_OBIS_D0_ID, CONF_OBIS_CODE, CONF_VALUE_REGEX, SmartMeterD0, obis_code, obis_d0_ns 7 | 8 | AUTO_LOAD = ["obis_d0"] 9 | 10 | SmartMeterD0TextSensor = obis_d0_ns.class_( 11 | "SmartMeterD0TextSensor", text_sensor.TextSensor, cg.Component) 12 | 13 | CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( 14 | { 15 | cv.GenerateID(): cv.declare_id(SmartMeterD0TextSensor), 16 | cv.GenerateID(CONF_OBIS_D0_ID): cv.use_id(SmartMeterD0), 17 | cv.Required(CONF_OBIS_CODE): obis_code, 18 | cv.Optional(CONF_VALUE_REGEX, default=".*"): cv.string, 19 | cv.Optional(CONF_TIMEOUT, default="5s"): cv.time_period, 20 | } 21 | ) 22 | 23 | 24 | async def to_code(config): 25 | var = cg.new_Pvariable( 26 | config[CONF_ID], 27 | config[CONF_OBIS_CODE], 28 | config[CONF_VALUE_REGEX], 29 | config[CONF_TIMEOUT].total_milliseconds, 30 | ) 31 | await cg.register_component(var, config) 32 | await text_sensor.register_text_sensor(var, config) 33 | obis_d0 = await cg.get_variable(config[CONF_OBIS_D0_ID]) 34 | cg.add(obis_d0.register_sensor(var)) 35 | -------------------------------------------------------------------------------- /components/obis_d0/tiny-regex-c/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /components/obis_d0/tiny-regex-c/README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/kokke/tiny-regex-c/workflows/CI/badge.svg) 2 | # tiny-regex-c 3 | 4 | Note: imported from https://raw.githubusercontent.com/kokke/tiny-regex-c 5 | 6 | # A small regex implementation in C 7 | ### Description 8 | Small and portable [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression) (regex) library written in C. 9 | 10 | Design is inspired by Rob Pike's regex-code for the book *"Beautiful Code"* [available online here](http://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html). 11 | 12 | Supports a subset of the syntax and semantics of the Python standard library implementation (the `re`-module). 13 | 14 | **I will gladly accept patches correcting bugs.** 15 | 16 | ### Design goals 17 | The main design goal of this library is to be small, correct, self contained and use few resources while retaining acceptable performance and feature completeness. Clarity of the code is also highly valued. 18 | 19 | ### Notable features and omissions 20 | - Small code and binary size: 500 SLOC, ~3kb binary for x86. Statically #define'd memory usage / allocation. 21 | - No use of dynamic memory allocation (i.e. no calls to `malloc` / `free`). 22 | - To avoid call-stack exhaustion, iterative searching is preferred over recursive by default (can be changed with a pre-processor flag). 23 | - No support for capturing groups or named capture: `(^Pgroup)` etc. 24 | - Thorough testing : [exrex](https://github.com/asciimoo/exrex) is used to randomly generate test-cases from regex patterns, which are fed into the regex code for verification. Try `make test` to generate a few thousand tests cases yourself. 25 | - Verification-harness for [KLEE Symbolic Execution Engine](https://klee.github.io), see [formal verification.md](https://github.com/kokke/tiny-regex-c/blob/master/formal_verification.md). 26 | - Provides character length of matches. 27 | - Compiled for x86 using GCC 7.2.0 and optimizing for size, the binary takes up ~2-3kb code space and allocates ~0.5kb RAM : 28 | ``` 29 | > gcc -Os -c re.c 30 | > size re.o 31 | text data bss dec hex filename 32 | 2404 0 304 2708 a94 re.o 33 | 34 | ``` 35 | 36 | 37 | 38 | ### API 39 | This is the public / exported API: 40 | ```C 41 | /* Typedef'd pointer to hide implementation details. */ 42 | typedef struct regex_t* re_t; 43 | 44 | /* Compiles regex string pattern to a regex_t-array. */ 45 | re_t re_compile(const char* pattern); 46 | 47 | /* Finds matches of the compiled pattern inside text. */ 48 | int re_matchp(re_t pattern, const char* text, int* matchlength); 49 | 50 | /* Finds matches of pattern inside text (compiles first automatically). */ 51 | int re_match(const char* pattern, const char* text, int* matchlength); 52 | ``` 53 | 54 | ### Supported regex-operators 55 | The following features / regex-operators are supported by this library. 56 | 57 | NOTE: inverted character classes are buggy - see the test harness for concrete examples. 58 | 59 | 60 | - `.` Dot, matches any character 61 | - `^` Start anchor, matches beginning of string 62 | - `$` End anchor, matches end of string 63 | - `*` Asterisk, match zero or more (greedy) 64 | - `+` Plus, match one or more (greedy) 65 | - `?` Question, match zero or one (non-greedy) 66 | - `[abc]` Character class, match if one of {'a', 'b', 'c'} 67 | - `[^abc]` Inverted class, match if NOT one of {'a', 'b', 'c'} 68 | - `[a-zA-Z]` Character ranges, the character set of the ranges { a-z | A-Z } 69 | - `\s` Whitespace, \t \f \r \n \v and spaces 70 | - `\S` Non-whitespace 71 | - `\w` Alphanumeric, [a-zA-Z0-9_] 72 | - `\W` Non-alphanumeric 73 | - `\d` Digits, [0-9] 74 | - `\D` Non-digits 75 | 76 | ### Usage 77 | Compile a regex from ASCII-string (char-array) to a custom pattern structure using `re_compile()`. 78 | 79 | Search a text-string for a regex and get an index into the string, using `re_match()` or `re_matchp()`. 80 | 81 | The returned index points to the first place in the string, where the regex pattern matches. 82 | 83 | The integer pointer passed will hold the length of the match. 84 | 85 | If the regular expression doesn't match, the matching function returns an index of -1 to indicate failure. 86 | 87 | ### Examples 88 | Example of usage: 89 | ```C 90 | /* Standard int to hold length of match */ 91 | int match_length; 92 | 93 | /* Standard null-terminated C-string to search: */ 94 | const char* string_to_search = "ahem.. 'hello world !' .."; 95 | 96 | /* Compile a simple regular expression using character classes, meta-char and greedy + non-greedy quantifiers: */ 97 | re_t pattern = re_compile("[Hh]ello [Ww]orld\\s*[!]?"); 98 | 99 | /* Check if the regex matches the text: */ 100 | int match_idx = re_matchp(pattern, string_to_search, &match_length); 101 | if (match_idx != -1) 102 | { 103 | printf("match at idx %i, %i chars long.\n", match_idx, match_length); 104 | } 105 | ``` 106 | 107 | For more usage examples I encourage you to look at the code in the `tests`-folder. 108 | 109 | ### TODO 110 | - Fix the implementation of inverted character classes. 111 | - Fix implementation of branches (`|`), and see if that can lead us closer to groups as well, e.g. `(a|b)+`. 112 | - Add `example.c` that demonstrates usage. 113 | - Add `tests/test_perf.c` for performance and time measurements. 114 | - Testing: Improve pattern rejection testing. 115 | 116 | ### FAQ 117 | - *Q: What differentiates this library from other C regex implementations?* 118 | 119 | A: Well, the small size for one. 500 lines of C-code compiling to 2-3kb ROM, using very little RAM. 120 | 121 | ### License 122 | All material in this repository is in the public domain. 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /doc/ace_3000_type_260.md: -------------------------------------------------------------------------------- 1 | # ACE 3000 Type 260 2 | 3 | UART configuration: 300 Baud, 7E1 4 | 5 | ## Example 6 | 7 | ```yaml 8 | external_components: 9 | - source: github://mampfes/esphome_obis_d0 10 | 11 | uart: 12 | id: uart_bus 13 | tx_pin: 1 14 | rx_pin: 3 15 | baud_rate: 300 16 | data_bits: 7 17 | parity: EVEN 18 | stop_bits: 1 19 | 20 | obis_d0: 21 | id: mysml 22 | uart_id: uart_bus 23 | optimize_size: true 24 | 25 | sensor: 26 | - platform: obis_d0 27 | name: "Energy" 28 | obis_d0_id: mysml 29 | obis_code: "1.8.0" 30 | unit_of_measurement: kWh 31 | accuracy_decimals: 1 32 | device_class: energy 33 | state_class: total_increasing 34 | value_regex: "\\d{6}\\.\\d{1}\\*kWh" 35 | # Filter, b/c sometimes NaN is received 36 | filters: 37 | - filter_out: nan 38 | 39 | # ACE energy meter needs to be triggered manually to send out data 40 | interval: 41 | - interval: 5min 42 | then: 43 | - uart.write: "/?!\r\n" 44 | ``` 45 | -------------------------------------------------------------------------------- /doc/easymeter_q3d.md: -------------------------------------------------------------------------------- 1 | # EasyMeter Q3D 2 | 3 | Tested configuration: EasyMeter Q3DA3002 V3.04 4 | 5 | UART configuration: 9600 Baud, 7E1 6 | 7 | ## Example 8 | 9 | ```yaml 10 | external_components: 11 | - source: github://mampfes/esphome_obis_d0 12 | 13 | uart: 14 | id: uart_bus 15 | rx_pin: GPIO16 16 | baud_rate: 9600 17 | data_bits: 7 18 | parity: EVEN 19 | stop_bits: 1 20 | 21 | obis_d0: 22 | id: my_sm 23 | uart_id: uart_bus 24 | 25 | sensor: 26 | - platform: obis_d0 27 | name: "Zaehlerstand" 28 | obis_d0_id: my_sm 29 | obis_code: "1-0:1.8.0*255" 30 | unit_of_measurement: kWh 31 | accuracy_decimals: 3 32 | state_class: total_increasing 33 | device_class: energy 34 | value_regex: "\d{8}\.\d{7}\*kWh" 35 | 36 | - platform: obis_d0 37 | name: "Gesamtleistung" 38 | obis_d0_id: my_sm 39 | obis_code: "1-0:1.7.0*255" 40 | unit_of_measurement: W 41 | accuracy_decimals: 0 42 | device_class: power 43 | state_class: measurement 44 | 45 | - platform: obis_d0 46 | name: "Leistung L1" 47 | obis_d0_id: my_sm 48 | obis_code: "1-0:21.7.0*255" 49 | unit_of_measurement: W 50 | accuracy_decimals: 0 51 | device_class: power 52 | state_class: measurement 53 | 54 | - platform: obis_d0 55 | name: "Leistung L2" 56 | obis_d0_id: my_sm 57 | obis_code: "1-0:41.7.0*255" 58 | unit_of_measurement: W 59 | accuracy_decimals: 0 60 | device_class: power 61 | state_class: measurement 62 | 63 | - platform: obis_d0 64 | name: "Leistung L3" 65 | obis_d0_id: my_sm 66 | obis_code: "1-0:61.7.0*255" 67 | unit_of_measurement: W 68 | accuracy_decimals: 0 69 | device_class: power 70 | state_class: measurement 71 | 72 | text_sensor: 73 | - platform: obis_d0 74 | name: "Device Identification" 75 | obis_d0_id: my_sm 76 | obis_code: "0-0:96.1.255*255" 77 | entity_category: diagnostic 78 | value_regex: "\w{14}" 79 | 80 | - platform: obis_d0 81 | name: "Manufacturer ID" 82 | obis_d0_id: my_sm 83 | obis_code: "id" 84 | entity_category: diagnostic 85 | ``` 86 | -------------------------------------------------------------------------------- /doc/ebz_dd3.md: -------------------------------------------------------------------------------- 1 | # eBZ DD3 Drehstromzähler 2 | 3 | ![eBZ DD3](ebz_dd3.png) 4 | 5 | UART configuration: 9600 Baud, 7E1 6 | 7 | This device has 2 D0 interfaces: 8 | 9 | - **MSB-Schnittstelle** at the top\ 10 | Outputs all readings (including in voltage) in full resolution. 11 | 12 | - **Info-Schnittstelle** at the front\ 13 | Outputs standard readings (excluding voltage) in limited resolution per default. To enable full resolution, the device has to be unlocked using a PIN. After a power blackdown the resolution switches back to limited resolution and has to be unlocked again. 14 | 15 | ## Example 16 | 17 | ```yaml 18 | external_components: 19 | - source: github://mampfes/esphome_obis_d0 20 | 21 | uart: 22 | id: my_uart 23 | rx_pin: GPIO16 24 | baud_rate: 9600 25 | data_bits: 7 26 | parity: EVEN 27 | stop_bits: 1 28 | 29 | obis_d0: 30 | id: my_sm 31 | uart_id: my_uart 32 | 33 | sensor: 34 | - !include common/sensor/wifi.yaml 35 | - !include common/sensor/uptime.yaml 36 | 37 | - platform: obis_d0 38 | name: "Consumed Energy" 39 | obis_d0_id: my_sm 40 | obis_code: "1-0:1.8.0*255" 41 | unit_of_measurement: kWh 42 | accuracy_decimals: 4 43 | state_class: total_increasing 44 | device_class: energy 45 | value_regex: "\\d{6}\\.\\d{8}\\*kWh" 46 | 47 | # - platform: obis_d0 48 | # name: "Consumed Energy Tariff 1" 49 | # obis_d0_id: my_sm 50 | # obis_code: "1-0:1.8.1*255" 51 | # unit_of_measurement: kWh 52 | # accuracy_decimals: 4 53 | # state_class: total_increasing 54 | # device_class: energy 55 | # value_regex: "\\d{6}\\.\\d{3}\\*kWh" 56 | 57 | # - platform: obis_d0 58 | # name: "Consumed Energy Tariff 2" 59 | # obis_d0_id: my_sm 60 | # obis_code: "1-0:1.8.2*255" 61 | # unit_of_measurement: kWh 62 | # accuracy_decimals: 4 63 | # state_class: total_increasing 64 | # device_class: energy 65 | # value_regex: "\\d{6}\\.\\d{3}\\*kWh" 66 | 67 | - platform: obis_d0 68 | name: "Provided Energy" 69 | obis_d0_id: my_sm 70 | obis_code: "1-0:2.8.0*255" 71 | unit_of_measurement: kWh 72 | accuracy_decimals: 4 73 | state_class: total_increasing 74 | device_class: energy 75 | value_regex: "\\d{6}\\.\\d{8}\\*kWh" 76 | 77 | # - platform: obis_d0 78 | # name: "Provided Energy Tariff 1" 79 | # obis_d0_id: my_sm 80 | # obis_code: "1-0:2.8.1*255" 81 | # unit_of_measurement: kWh 82 | # accuracy_decimals: 4 83 | # state_class: total_increasing 84 | # device_class: energy 85 | # value_regex: "\\d{6}\\.\\d{3}\\*kWh" 86 | 87 | # - platform: obis_d0 88 | # name: "Provided Energy Tariff 2" 89 | # obis_d0_id: my_sm 90 | # obis_code: "1-0:2.8.2*255" 91 | # unit_of_measurement: kWh 92 | # accuracy_decimals: 4 93 | # state_class: total_increasing 94 | # device_class: energy 95 | # value_regex: "\\d{6}\\.\\d{3}\\*kWh" 96 | 97 | - platform: obis_d0 98 | name: "Power" 99 | obis_d0_id: my_sm 100 | obis_code: "1-0:16.7.0*255" 101 | unit_of_measurement: W 102 | accuracy_decimals: 2 103 | state_class: measurement 104 | device_class: power 105 | value_regex: "-?\\d{6}\\.\\d{2}\\*W" 106 | 107 | - platform: obis_d0 108 | name: "Power L1" 109 | obis_d0_id: my_sm 110 | obis_code: "1-0:36.7.0*255" 111 | unit_of_measurement: W 112 | accuracy_decimals: 2 113 | state_class: measurement 114 | device_class: power 115 | value_regex: "-?\\d{6}\\.\\d{2}\\*W" 116 | 117 | - platform: obis_d0 118 | name: "Power L2" 119 | obis_d0_id: my_sm 120 | obis_code: "1-0:56.7.0*255" 121 | unit_of_measurement: W 122 | accuracy_decimals: 2 123 | state_class: measurement 124 | device_class: power 125 | value_regex: "-?\\d{6}\\.\\d{2}\\*W" 126 | 127 | - platform: obis_d0 128 | name: "Power L3" 129 | obis_d0_id: my_sm 130 | obis_code: "1-0:76.7.0*255" 131 | unit_of_measurement: W 132 | accuracy_decimals: 2 133 | state_class: measurement 134 | device_class: power 135 | value_regex: "-?\\d{6}\\.\\d{2}\\*W" 136 | 137 | - platform: obis_d0 138 | name: "Voltage L1" 139 | obis_d0_id: my_sm 140 | obis_code: "1-0:32.7.0*255" 141 | unit_of_measurement: V 142 | accuracy_decimals: 1 143 | state_class: measurement 144 | device_class: voltage 145 | value_regex: "\\d{3}\\.\\d{1}\\*V" 146 | 147 | - platform: obis_d0 148 | name: "Voltage L2" 149 | obis_d0_id: my_sm 150 | obis_code: "1-0:52.7.0*255" 151 | unit_of_measurement: V 152 | accuracy_decimals: 1 153 | state_class: measurement 154 | device_class: voltage 155 | value_regex: "\\d{3}\\.\\d{1}\\*V" 156 | 157 | - platform: obis_d0 158 | name: "Voltage L3" 159 | obis_d0_id: my_sm 160 | obis_code: "1-0:72.7.0*255" 161 | unit_of_measurement: V 162 | accuracy_decimals: 1 163 | state_class: measurement 164 | device_class: voltage 165 | value_regex: "\\d{3}\\.\\d{1}\\*V" 166 | 167 | - platform: obis_d0 168 | name: "Status" 169 | obis_d0_id: my_sm 170 | obis_code: "1-0:96.5.0*255" 171 | value_regex: "[0-9a-fA-F]{8}" 172 | format: hex 173 | 174 | - platform: obis_d0 175 | name: "Time of operation" 176 | obis_d0_id: my_sm 177 | obis_code: "0-0:96.8.0*255" 178 | format: hex 179 | disabled_by_default: true 180 | entity_category: diagnostic 181 | value_regex: "[0-9a-fA-F]{8}" 182 | 183 | text_sensor: 184 | # - platform: obis_d0 185 | # name: "Owner Identification" 186 | # obis_d0_id: my_sm 187 | # obis_code: "1-0:0.0.0*255" 188 | # entity_category: diagnostic 189 | # value_regex: "\\w{14}" 190 | 191 | - platform: obis_d0 192 | name: "Device Identification" 193 | obis_d0_id: my_sm 194 | obis_code: "1-0:96.1.0*255" 195 | entity_category: diagnostic 196 | value_regex: "\\w{14}" 197 | ``` 198 | -------------------------------------------------------------------------------- /doc/ebz_dd3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/esphome_obis_d0/f66604849ba36d98ae51bbfcd0deb53d2f23c784/doc/ebz_dd3.png -------------------------------------------------------------------------------- /doc/ir_reader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/esphome_obis_d0/f66604849ba36d98ae51bbfcd0deb53d2f23c784/doc/ir_reader.jpg -------------------------------------------------------------------------------- /doc/iskra_mt174.md: -------------------------------------------------------------------------------- 1 | # ISKRA MT174 2 | 3 | ![ISKRA MT174](iskra_mt174.png) 4 | 5 | UART configuration: 300 Baud, 7E1 6 | 7 | ## Example 8 | 9 | ```yaml 10 | external_components: 11 | - source: github://mampfes/esphome_obis_d0 12 | 13 | uart: 14 | id: my_uart 15 | rx_pin: GPIO16 16 | tx_pin: GPIO17 17 | baud_rate: 300 18 | data_bits: 7 19 | parity: EVEN 20 | stop_bits: 1 21 | 22 | obis_d0: 23 | id: my_sm 24 | uart_id: my_uart 25 | 26 | ## --------------------------------------------------- 27 | # The device needs to be triggered to send out data. 28 | # We can use the integrated "interval" component for that: 29 | ## --------------------------------------------------- 30 | interval: 31 | - interval: 60sec 32 | then: 33 | - uart.write: 34 | id: my_uart 35 | data: [0x2F, 0x3F, 0x21, 0x0D, 0x0A] 36 | 37 | sensor: 38 | - !include common/sensor/wifi.yaml 39 | - !include common/sensor/uptime.yaml 40 | 41 | # 1.8.0(0011404.409*kWh) Positive active energy (A+) total [kWh] 42 | - platform: obis_d0 43 | obis_d0_id: my_sm 44 | name: "Verbrauch" 45 | obis_code: "1.8.10" 46 | unit_of_measurement: kWh 47 | accuracy_decimals: 3 48 | device_class: energy 49 | state_class: total_increasing 50 | value_regex: "(\\d{7}\\.\\d{3}\\*kWh)" 51 | 52 | # 1.8.1(0011404.409*kWh) Positive active energy (A+) in tariff HT [kWh] 53 | - platform: obis_d0 54 | obis_d0_id: my_sm 55 | name: "Verbrauch HT" 56 | obis_code: "1.8.1" 57 | unit_of_measurement: kWh 58 | accuracy_decimals: 3 59 | device_class: energy 60 | state_class: total_increasing 61 | value_regex: "(\\d{7}\\.\\d{3}\\*kWh)" 62 | 63 | # 1.8.2(0023813.725*kWh) Positive active energy (A+) in tariff NT [kWh] 64 | - platform: obis_d0 65 | obis_d0_id: my_sm 66 | name: "Verbrauch NT" 67 | obis_code: "1.8.2" 68 | unit_of_measurement: kWh 69 | accuracy_decimals: 3 70 | device_class: energy 71 | state_class: total_increasing 72 | value_regex: "\\d{7}\\.\\d{3}\\*kWh" 73 | 74 | # 2.8.0(0015608.962*kWh) Negative active energy (A-) total [kWh] 75 | - platform: obis_d0 76 | obis_d0_id: my_sm 77 | name: "Einspeisung" 78 | obis_code: "2.8.0" 79 | unit_of_measurement: kWh 80 | accuracy_decimals: 3 81 | device_class: energy 82 | state_class: total_increasing 83 | value_regex: "\\d{7}\\.\\d{3}\\*kWh" 84 | 85 | # 2.8.1(0015608.962*kWh) Negative active energy (A-) in tariff T1 [kWh] 86 | - platform: obis_d0 87 | obis_d0_id: my_sm 88 | name: "Einspeisung T1" 89 | obis_code: "2.8.1" 90 | unit_of_measurement: kWh 91 | accuracy_decimals: 3 92 | device_class: energy 93 | state_class: total_increasing 94 | value_regex: "\\d{7}\\.\\d{3}\\*kWh" 95 | 96 | # 2.8.2(0000900.569*kWh) Negative active energy (A-) in tariff T2 [kWh] 97 | - platform: obis_d0 98 | obis_d0_id: my_sm 99 | name: "Einspeisung T2" 100 | obis_code: "2.8.2" 101 | unit_of_measurement: kWh 102 | accuracy_decimals: 3 103 | device_class: energy 104 | state_class: total_increasing 105 | value_regex: "\\d{7}\\.\\d{3}\\*kWh" 106 | 107 | text_sensor: 108 | # 0.9.2(0230223) Date (1YY.MM.DD) 109 | - platform: obis_d0 110 | name: "MT174Mdate" 111 | internal: true 112 | obis_d0_id: my_sm 113 | obis_code: "0.9.2" 114 | value_regex: "\\d{7}" 115 | filters: 116 | - prepend: "2" 117 | on_value: 118 | then: 119 | - lambda: |- 120 | std::string result = x.substr(0,4) + "-" + x.substr(4,2) + "-" + x.substr(6,2); 121 | id(thedate).publish_state(result); 122 | 123 | 124 | # 0.9.1(130203) Current time (hh:mm:ss) 125 | - platform: obis_d0 126 | name: "MT174Mtime" 127 | internal: true 128 | obis_d0_id: my_sm 129 | obis_code: "0.9.1" 130 | value_regex: "\\d{6}" 131 | on_value: 132 | then: 133 | - lambda: |- 134 | std::string result = x.substr(0,2) + ":" + x.substr(2,2) + ":" + x.substr(4,2); 135 | id(thetime).publish_state(result); 136 | 137 | - platform: template 138 | name: Messdatum 139 | id: thedate 140 | entity_category: "diagnostic" 141 | 142 | - platform: template 143 | name: Messzeit 144 | id: thetime 145 | entity_category: "diagnostic" 146 | ``` 147 | -------------------------------------------------------------------------------- /doc/iskra_mt174.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mampfes/esphome_obis_d0/f66604849ba36d98ae51bbfcd0deb53d2f23c784/doc/iskra_mt174.png -------------------------------------------------------------------------------- /doc/logarex.md: -------------------------------------------------------------------------------- 1 | # Logarex LK11 Drehstromzähler, LK13 Wechselstromzähler 2 | 3 | UART configuration: 9600 Baud, 7E1 4 | 5 | This device has an optical D0 interface at the front. To enable the output of complete data records (full resolution and all available values), the ```INF``` parameter has to be set to ```on```. 6 | 7 | ## Example 8 | 9 | ```yaml 10 | external_components: 11 | - source: github://mampfes/esphome_obis_d0 12 | 13 | uart: 14 | id: my_uart 15 | rx_pin: GPIO16 16 | baud_rate: 9600 17 | data_bits: 7 18 | parity: EVEN 19 | stop_bits: 1 20 | 21 | obis_d0: 22 | id: my_sm 23 | uart_id: my_uart 24 | 25 | sensor: 26 | - !include common/sensor/wifi.yaml 27 | - !include common/sensor/uptime.yaml 28 | 29 | - platform: obis_d0 30 | name: "Consumed Energy" 31 | obis_d0_id: my_sm 32 | obis_code: "1-0:1.8.0*255" 33 | unit_of_measurement: kWh 34 | accuracy_decimals: 4 35 | state_class: total_increasing 36 | device_class: energy 37 | value_regex: "\\d{6}\\.\\d{4}\\*kWh" 38 | 39 | # - platform: obis_d0 40 | # name: "Consumed Energy Tariff 1" 41 | # obis_d0_id: my_sm 42 | # obis_code: "1-0:1.8.1*255" 43 | # unit_of_measurement: kWh 44 | # accuracy_decimals: 4 45 | # state_class: total_increasing 46 | # device_class: energy 47 | # value_regex: "\\d{6}\\.\\d{4}\\*kWh" 48 | 49 | # - platform: obis_d0 50 | # name: "Consumed Energy Tariff 2" 51 | # obis_d0_id: my_sm 52 | # obis_code: "1-0:1.8.2*255" 53 | # unit_of_measurement: kWh 54 | # accuracy_decimals: 4 55 | # state_class: total_increasing 56 | # device_class: energy 57 | # value_regex: "\\d{6}\\.\\d{4}\\*kWh" 58 | 59 | - platform: obis_d0 60 | name: "Provided Energy" 61 | obis_d0_id: my_sm 62 | obis_code: "1-0:2.8.0*255" 63 | unit_of_measurement: kWh 64 | accuracy_decimals: 4 65 | state_class: total_increasing 66 | device_class: energy 67 | value_regex: "\\d{6}\\.\\d{4}\\*kWh" 68 | 69 | - platform: obis_d0 70 | name: "Power" 71 | obis_d0_id: my_sm 72 | obis_code: "1-0:16.7.0*255" 73 | unit_of_measurement: W 74 | accuracy_decimals: 0 75 | state_class: measurement 76 | device_class: power 77 | value_regex: "-?\\d{6}\\*W" 78 | 79 | - platform: obis_d0 80 | name: "Voltage L1" 81 | obis_d0_id: my_sm 82 | obis_code: "1-0:32.7.0*255" 83 | unit_of_measurement: V 84 | accuracy_decimals: 1 85 | state_class: measurement 86 | device_class: voltage 87 | value_regex: "\\d{3}\\.\\d{1}\\*V" 88 | 89 | - platform: obis_d0 90 | name: "Voltage L2" 91 | obis_d0_id: my_sm 92 | obis_code: "1-0:52.7.0*255" 93 | unit_of_measurement: V 94 | accuracy_decimals: 1 95 | state_class: measurement 96 | device_class: voltage 97 | value_regex: "\\d{3}\\.\\d{1}\\*V" 98 | 99 | - platform: obis_d0 100 | name: "Voltage L3" 101 | obis_d0_id: my_sm 102 | obis_code: "1-0:72.7.0*255" 103 | unit_of_measurement: V 104 | accuracy_decimals: 1 105 | state_class: measurement 106 | device_class: voltage 107 | value_regex: "\\d{3}\\.\\d{1}\\*V" 108 | 109 | - platform: obis_d0 110 | name: "Current L1" 111 | obis_d0_id: my_sm 112 | obis_code: "1-0:31.7.0*255" 113 | unit_of_measurement: A 114 | accuracy_decimals: 2 115 | state_class: measurement 116 | device_class: current 117 | value_regex: "-?\\d{3}\\.\\d{2}\\*A" 118 | 119 | - platform: obis_d0 120 | name: "Current L2" 121 | obis_d0_id: my_sm 122 | obis_code: "1-0:51.7.0*255" 123 | unit_of_measurement: A 124 | accuracy_decimals: 2 125 | state_class: measurement 126 | device_class: current 127 | value_regex: "-?\\d{3}\\.\\d{2}\\*A" 128 | 129 | - platform: obis_d0 130 | name: "Current L3" 131 | obis_d0_id: my_sm 132 | obis_code: "1-0:71.7.0*255" 133 | unit_of_measurement: A 134 | accuracy_decimals: 2 135 | state_class: measurement 136 | device_class: current 137 | value_regex: "-?\\d{3}\\.\\d{2}\\*A" 138 | 139 | - platform: obis_d0 140 | name: "Phase Angle UL2-UL1" 141 | obis_d0_id: my_sm 142 | obis_code: "1-0:81.7.1*255" 143 | unit_of_measurement: ° 144 | accuracy_decimals: 0 145 | state_class: measurement 146 | value_regex: "\\d{3}\\*deg" 147 | 148 | - platform: obis_d0 149 | name: "Phase Angle UL3-UL1" 150 | obis_d0_id: my_sm 151 | obis_code: "1-0:81.7.2*255" 152 | unit_of_measurement: ° 153 | accuracy_decimals: 0 154 | state_class: measurement 155 | value_regex: "\\d{3}\\*deg" 156 | 157 | - platform: obis_d0 158 | name: "Phase Angle IL1-UL1" 159 | obis_d0_id: my_sm 160 | obis_code: "1-0:81.7.4*255" 161 | unit_of_measurement: ° 162 | accuracy_decimals: 0 163 | state_class: measurement 164 | value_regex: "\\d{3}\\*deg" 165 | 166 | - platform: obis_d0 167 | name: "Phase Angle IL2-UL2" 168 | obis_d0_id: my_sm 169 | obis_code: "1-0:81.7.15*255" 170 | unit_of_measurement: ° 171 | accuracy_decimals: 0 172 | state_class: measurement 173 | value_regex: "\\d{3}\\*deg" 174 | 175 | - platform: obis_d0 176 | name: "Phase Angle IL3-UL3" 177 | obis_d0_id: my_sm 178 | obis_code: "1-0:81.7.26*255" 179 | unit_of_measurement: ° 180 | accuracy_decimals: 0 181 | state_class: measurement 182 | value_regex: "\\d{3}\\*deg" 183 | 184 | - platform: obis_d0 185 | name: "Power Frequency" 186 | obis_d0_id: my_sm 187 | obis_code: "1-0:14.7.0*255" 188 | unit_of_measurement: Hz 189 | accuracy_decimals: 1 190 | state_class: measurement 191 | device_class: frequency 192 | value_regex: "\\d{2}\\.\\d{1}\\*Hz" 193 | 194 | - platform: obis_d0 195 | name: "Consumed Power last 1d" 196 | obis_d0_id: my_sm 197 | obis_code: "1-0:1.8.0*96" 198 | unit_of_measurement: kWh 199 | accuracy_decimals: 1 200 | state_class: measurement 201 | device_class: energy 202 | value_regex: "\\d{5}\\.\\d{1}\\*kWh" 203 | 204 | - platform: obis_d0 205 | name: "Consumed Power last 7d" 206 | obis_d0_id: my_sm 207 | obis_code: "1-0:1.8.0*97" 208 | unit_of_measurement: kWh 209 | accuracy_decimals: 1 210 | state_class: measurement 211 | device_class: energy 212 | value_regex: "\\d{5}\\.\\d{1}\\*kWh" 213 | 214 | - platform: obis_d0 215 | name: "Consumed Power last 30d" 216 | obis_d0_id: my_sm 217 | obis_code: "1-0:1.8.0*98" 218 | unit_of_measurement: kWh 219 | accuracy_decimals: 1 220 | state_class: measurement 221 | device_class: energy 222 | value_regex: "\\d{5}\\.\\d{1}\\*kWh" 223 | 224 | - platform: obis_d0 225 | name: "Consumed Power last 365d" 226 | obis_d0_id: my_sm 227 | obis_code: "1-0:1.8.0*99" 228 | unit_of_measurement: kWh 229 | accuracy_decimals: 1 230 | state_class: measurement 231 | device_class: energy 232 | value_regex: "\\d{5}\\.\\d{1}\\*kWh" 233 | 234 | - platform: obis_d0 235 | name: "Consumed Power last reset" 236 | obis_d0_id: my_sm 237 | obis_code: "1-0:1.8.0*100" 238 | unit_of_measurement: kWh 239 | accuracy_decimals: 1 240 | state_class: measurement 241 | device_class: energy 242 | value_regex: "\\d{5}\\.\\d{1}\\*kWh" 243 | 244 | - platform: obis_d0 245 | name: "CRC" 246 | obis_d0_id: my_sm 247 | obis_code: "1-0:96.90.2*255" 248 | value_regex: "[0-9a-fA-F]{4}" 249 | format: hex 250 | 251 | - platform: obis_d0 252 | name: "Status" 253 | obis_d0_id: my_sm 254 | obis_code: "1-0:97.97.0*255" 255 | value_regex: "[0-9a-fA-F]{8}" 256 | format: hex 257 | 258 | text_sensor: 259 | - platform: obis_d0 260 | name: "Firmware Version" 261 | obis_d0_id: my_sm 262 | obis_code: "1-0:0.2.0*255" 263 | entity_category: diagnostic 264 | value_regex: "[0-9a-fA-F.,]{20}" 265 | 266 | - platform: obis_d0 267 | name: "Device Identification" 268 | obis_d0_id: my_sm 269 | obis_code: "1-0:96.1.0*255" 270 | entity_category: diagnostic 271 | value_regex: "\\w{16}" 272 | ``` 273 | --------------------------------------------------------------------------------