initial commit.
This commit is contained in:
parent
cbb9131ec8
commit
ced79a4af9
137
.clang-format
Normal file
137
.clang-format
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
Language: Cpp
|
||||||
|
AccessModifierOffset: -1
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignConsecutiveAssignments: false
|
||||||
|
AlignConsecutiveDeclarations: false
|
||||||
|
AlignEscapedNewlines: DontAlign
|
||||||
|
AlignOperands: true
|
||||||
|
AlignTrailingComments: true
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AllowShortBlocksOnASingleLine: false
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
AllowShortIfStatementsOnASingleLine: false
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AlwaysBreakAfterReturnType: None
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
AlwaysBreakTemplateDeclarations: MultiLine
|
||||||
|
BinPackArguments: true
|
||||||
|
BinPackParameters: true
|
||||||
|
BraceWrapping:
|
||||||
|
AfterClass: false
|
||||||
|
AfterControlStatement: false
|
||||||
|
AfterEnum: false
|
||||||
|
AfterFunction: false
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
AfterStruct: false
|
||||||
|
AfterUnion: false
|
||||||
|
AfterExternBlock: false
|
||||||
|
BeforeCatch: false
|
||||||
|
BeforeElse: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: true
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeBraces: Attach
|
||||||
|
BreakBeforeInheritanceComma: false
|
||||||
|
BreakInheritanceList: BeforeColon
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakConstructorInitializersBeforeComma: false
|
||||||
|
BreakConstructorInitializers: BeforeColon
|
||||||
|
BreakAfterJavaFieldAnnotations: false
|
||||||
|
BreakStringLiterals: true
|
||||||
|
ColumnLimit: 120
|
||||||
|
CommentPragmas: '^ IWYU pragma:'
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||||
|
ConstructorInitializerIndentWidth: 4
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
Cpp11BracedListStyle: true
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
DisableFormat: false
|
||||||
|
ExperimentalAutoDetectBinPacking: false
|
||||||
|
FixNamespaceComments: true
|
||||||
|
ForEachMacros:
|
||||||
|
- foreach
|
||||||
|
- Q_FOREACH
|
||||||
|
- BOOST_FOREACH
|
||||||
|
IncludeBlocks: Preserve
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^<ext/.*\.h>'
|
||||||
|
Priority: 2
|
||||||
|
- Regex: '^<.*\.h>'
|
||||||
|
Priority: 1
|
||||||
|
- Regex: '^<.*'
|
||||||
|
Priority: 2
|
||||||
|
- Regex: '.*'
|
||||||
|
Priority: 3
|
||||||
|
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||||
|
IndentCaseLabels: true
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentWidth: 2
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
MacroBlockBegin: ''
|
||||||
|
MacroBlockEnd: ''
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
NamespaceIndentation: None
|
||||||
|
PenaltyBreakAssignment: 2
|
||||||
|
PenaltyBreakBeforeFirstCallParameter: 1
|
||||||
|
PenaltyBreakComment: 300
|
||||||
|
PenaltyBreakFirstLessLess: 120
|
||||||
|
PenaltyBreakString: 1000
|
||||||
|
PenaltyBreakTemplateDeclaration: 10
|
||||||
|
PenaltyExcessCharacter: 1000000
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 2000
|
||||||
|
PointerAlignment: Right
|
||||||
|
RawStringFormats:
|
||||||
|
- Language: Cpp
|
||||||
|
Delimiters:
|
||||||
|
- cc
|
||||||
|
- CC
|
||||||
|
- cpp
|
||||||
|
- Cpp
|
||||||
|
- CPP
|
||||||
|
- 'c++'
|
||||||
|
- 'C++'
|
||||||
|
CanonicalDelimiter: ''
|
||||||
|
BasedOnStyle: google
|
||||||
|
- Language: TextProto
|
||||||
|
Delimiters:
|
||||||
|
- pb
|
||||||
|
- PB
|
||||||
|
- proto
|
||||||
|
- PROTO
|
||||||
|
EnclosingFunctions:
|
||||||
|
- EqualsProto
|
||||||
|
- EquivToProto
|
||||||
|
- PARSE_PARTIAL_TEXT_PROTO
|
||||||
|
- PARSE_TEST_PROTO
|
||||||
|
- PARSE_TEXT_PROTO
|
||||||
|
- ParseTextOrDie
|
||||||
|
- ParseTextProtoOrDie
|
||||||
|
CanonicalDelimiter: ''
|
||||||
|
BasedOnStyle: google
|
||||||
|
ReflowComments: true
|
||||||
|
SortIncludes: false
|
||||||
|
SortUsingDeclarations: false
|
||||||
|
SpaceAfterCStyleCast: true
|
||||||
|
SpaceAfterTemplateKeyword: false
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCpp11BracedList: false
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: true
|
||||||
|
SpaceInEmptyParentheses: false
|
||||||
|
SpacesBeforeTrailingComments: 2
|
||||||
|
SpacesInAngles: false
|
||||||
|
SpacesInContainerLiterals: false
|
||||||
|
SpacesInCStyleCastParentheses: false
|
||||||
|
SpacesInParentheses: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
Standard: Auto
|
||||||
|
TabWidth: 2
|
||||||
|
UseTab: Never
|
||||||
160
.clang-tidy
Normal file
160
.clang-tidy
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
Checks: >-
|
||||||
|
*,
|
||||||
|
-abseil-*,
|
||||||
|
-altera-*,
|
||||||
|
-android-*,
|
||||||
|
-boost-*,
|
||||||
|
-bugprone-narrowing-conversions,
|
||||||
|
-bugprone-signed-char-misuse,
|
||||||
|
-cert-dcl50-cpp,
|
||||||
|
-cert-err58-cpp,
|
||||||
|
-cert-oop57-cpp,
|
||||||
|
-cert-str34-c,
|
||||||
|
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
||||||
|
-clang-analyzer-osx.*,
|
||||||
|
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
||||||
|
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
||||||
|
-clang-diagnostic-shadow-field,
|
||||||
|
-clang-diagnostic-unused-const-variable,
|
||||||
|
-clang-diagnostic-unused-parameter,
|
||||||
|
-concurrency-*,
|
||||||
|
-cppcoreguidelines-avoid-c-arrays,
|
||||||
|
-cppcoreguidelines-avoid-magic-numbers,
|
||||||
|
-cppcoreguidelines-init-variables,
|
||||||
|
-cppcoreguidelines-macro-usage,
|
||||||
|
-cppcoreguidelines-narrowing-conversions,
|
||||||
|
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||||
|
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||||
|
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||||
|
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||||
|
-cppcoreguidelines-pro-type-const-cast,
|
||||||
|
-cppcoreguidelines-pro-type-cstyle-cast,
|
||||||
|
-cppcoreguidelines-pro-type-member-init,
|
||||||
|
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||||
|
-cppcoreguidelines-pro-type-static-cast-downcast,
|
||||||
|
-cppcoreguidelines-pro-type-union-access,
|
||||||
|
-cppcoreguidelines-pro-type-vararg,
|
||||||
|
-cppcoreguidelines-special-member-functions,
|
||||||
|
-fuchsia-multiple-inheritance,
|
||||||
|
-fuchsia-overloaded-operator,
|
||||||
|
-fuchsia-statically-constructed-objects,
|
||||||
|
-fuchsia-default-arguments-declarations,
|
||||||
|
-fuchsia-default-arguments-calls,
|
||||||
|
-google-build-using-namespace,
|
||||||
|
-google-explicit-constructor,
|
||||||
|
-google-readability-braces-around-statements,
|
||||||
|
-google-readability-casting,
|
||||||
|
-google-readability-namespace-comments,
|
||||||
|
-google-readability-todo,
|
||||||
|
-google-runtime-references,
|
||||||
|
-hicpp-*,
|
||||||
|
-llvm-else-after-return,
|
||||||
|
-llvm-header-guard,
|
||||||
|
-llvm-include-order,
|
||||||
|
-llvm-qualified-auto,
|
||||||
|
-llvmlibc-*,
|
||||||
|
-misc-non-private-member-variables-in-classes,
|
||||||
|
-misc-no-recursion,
|
||||||
|
-misc-unused-parameters,
|
||||||
|
-modernize-avoid-c-arrays,
|
||||||
|
-modernize-avoid-bind,
|
||||||
|
-modernize-concat-nested-namespaces,
|
||||||
|
-modernize-return-braced-init-list,
|
||||||
|
-modernize-use-auto,
|
||||||
|
-modernize-use-default-member-init,
|
||||||
|
-modernize-use-equals-default,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-modernize-use-nodiscard,
|
||||||
|
-mpi-*,
|
||||||
|
-objc-*,
|
||||||
|
-readability-convert-member-functions-to-static,
|
||||||
|
-readability-else-after-return,
|
||||||
|
-readability-function-cognitive-complexity,
|
||||||
|
-readability-implicit-bool-conversion,
|
||||||
|
-readability-isolate-declaration,
|
||||||
|
-readability-magic-numbers,
|
||||||
|
-readability-make-member-function-const,
|
||||||
|
-readability-redundant-string-init,
|
||||||
|
-readability-uppercase-literal-suffix,
|
||||||
|
-readability-use-anyofallof,
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
AnalyzeTemporaryDtors: false
|
||||||
|
FormatStyle: google
|
||||||
|
CheckOptions:
|
||||||
|
- key: google-readability-braces-around-statements.ShortStatementLines
|
||||||
|
value: '1'
|
||||||
|
- key: google-readability-function-size.StatementThreshold
|
||||||
|
value: '800'
|
||||||
|
- key: google-runtime-int.TypeSuffix
|
||||||
|
value: '_t'
|
||||||
|
- key: llvm-namespace-comment.ShortNamespaceLines
|
||||||
|
value: '10'
|
||||||
|
- key: llvm-namespace-comment.SpacesBeforeComments
|
||||||
|
value: '2'
|
||||||
|
- key: modernize-loop-convert.MaxCopySize
|
||||||
|
value: '16'
|
||||||
|
- key: modernize-loop-convert.MinConfidence
|
||||||
|
value: reasonable
|
||||||
|
- key: modernize-loop-convert.NamingStyle
|
||||||
|
value: CamelCase
|
||||||
|
- key: modernize-pass-by-value.IncludeStyle
|
||||||
|
value: llvm
|
||||||
|
- key: modernize-replace-auto-ptr.IncludeStyle
|
||||||
|
value: llvm
|
||||||
|
- key: modernize-use-nullptr.NullMacros
|
||||||
|
value: 'NULL'
|
||||||
|
- key: modernize-make-unique.MakeSmartPtrFunction
|
||||||
|
value: 'make_unique'
|
||||||
|
- key: modernize-make-unique.MakeSmartPtrFunctionHeader
|
||||||
|
value: 'esphome/core/helpers.h'
|
||||||
|
- key: readability-braces-around-statements.ShortStatementLines
|
||||||
|
value: 2
|
||||||
|
- key: readability-identifier-naming.LocalVariableCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.StructCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.EnumCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.EnumConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.StaticConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.StaticVariableCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.GlobalConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.ParameterCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.PrivateMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.PrivateMemberSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.PrivateMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.PrivateMethodSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.ClassMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMemberSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.FunctionCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMethodSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.VirtualMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.VirtualMethodSuffix
|
||||||
|
value: ''
|
||||||
|
- key: readability-qualified-auto.AddConstToQualified
|
||||||
|
value: 0
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Gitignore settings for ESPHome
|
||||||
|
# This is an example and may include too much for your use-case.
|
||||||
|
# You can modify this file to suit your needs.
|
||||||
|
/.esphome/
|
||||||
|
/secrets.yaml
|
||||||
|
|
||||||
|
**/__pycache__/**
|
||||||
180
README.md
180
README.md
@ -1,2 +1,178 @@
|
|||||||
# esphome-truma_inetbox
|
# ESPHome truma_inetbox component
|
||||||
ESPHome component to remote control Truma CP Plus Heater
|
|
||||||
|
ESPHome component to remote control Truma CP Plus Heater by simulating a Truma iNet box.
|
||||||
|
|
||||||
|
See [1](https://github.com/danielfett/inetbox.py) and [2](https://github.com/mc0110/inetbox2mqtt) for great documentation about how to connect an CP Plus to an ESP32.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This project is based on the work of the [WomoLIN project](https://github.com/muccc/WomoLIN) and [mc0110 inetbox.py](https://github.com/danielfett/inetbox.py), especially the initial protocol decoding and the inet box log files.
|
||||||
|
|
||||||
|
## Example configuation
|
||||||
|
|
||||||
|
This example is just for connecting ESPHome to the CP Plus. See [truma.yaml](/truma.yaml) for an example config with all possible things configured.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
esphome:
|
||||||
|
name: "esphome-truma"
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: github://Fabian-Schmidt/esphome-truma_inetbox
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: mhetesp32devkit
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: lin_uart_bus
|
||||||
|
baud_rate: 9600
|
||||||
|
stop_bits: 2
|
||||||
|
|
||||||
|
truma_inetbox:
|
||||||
|
uart_id: lin_uart_bus
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "CP Plus alive"
|
||||||
|
type: CP_PLUS_CONNECTED
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Current Room Temperature"
|
||||||
|
type: CURRENT_ROOM_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Current Water Temperature"
|
||||||
|
type: CURRENT_WATER_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Room Temperature"
|
||||||
|
type: TARGET_ROOM_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Water Temperature"
|
||||||
|
type: TARGET_WATER_TEMPERATURE
|
||||||
|
```
|
||||||
|
|
||||||
|
## ESPHome components
|
||||||
|
|
||||||
|
This project contains the following ESPHome components:
|
||||||
|
|
||||||
|
- `uart` will overwrite the default uart component to expose internal fields.
|
||||||
|
- `truma_inetbox` has the following settings:
|
||||||
|
- `cs_pin` (optional) if you connect the pin of your lin driver chip.
|
||||||
|
- `fault_pin` (optional) if you connect the pin of your lin driver chip.
|
||||||
|
- `on_heater_message` (optional) [ESPHome Trigger](https://esphome.io/guides/automations.html) when a message from CP Plus is recieved.
|
||||||
|
|
||||||
|
### Binary sensor
|
||||||
|
|
||||||
|
Binary sensors are read-only.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
binary_sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "CP Plus alive"
|
||||||
|
type: CP_PLUS_CONNECTED
|
||||||
|
```
|
||||||
|
|
||||||
|
The following `type` values are available:
|
||||||
|
|
||||||
|
- `CP_PLUS_CONNECTED`
|
||||||
|
- `HEATER_ROOM`
|
||||||
|
- `HEATER_WATER`
|
||||||
|
- `HEATER_GAS`
|
||||||
|
- `HEATER_MIX_1`
|
||||||
|
- `HEATER_MIX_2`
|
||||||
|
- `HEATER_ELECTRICITY`
|
||||||
|
- `TIMER_ACTIVE`
|
||||||
|
- `TIMER_ROOM`
|
||||||
|
- `TIMER_WATER`
|
||||||
|
|
||||||
|
### Climate
|
||||||
|
|
||||||
|
Climate components support read and write.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
climate:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Truma Room"
|
||||||
|
type: ROOM
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Truma Water"
|
||||||
|
type: WATER
|
||||||
|
```
|
||||||
|
|
||||||
|
The following `type` values are available:
|
||||||
|
|
||||||
|
- `ROOM`
|
||||||
|
- `WATER`
|
||||||
|
|
||||||
|
### Number
|
||||||
|
|
||||||
|
Number components support read and write.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
number:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Room Temperature"
|
||||||
|
type: TARGET_ROOM_TEMPERATURE
|
||||||
|
```
|
||||||
|
|
||||||
|
The following `type` values are available:
|
||||||
|
|
||||||
|
- `TARGET_ROOM_TEMPERATURE`
|
||||||
|
- `TARGET_WATER_TEMPERATURE`
|
||||||
|
- `ELECTRIC_POWER_LEVEL`
|
||||||
|
|
||||||
|
### Sensor
|
||||||
|
|
||||||
|
Sensors are read-only.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Current Room Temperature"
|
||||||
|
type: CURRENT_ROOM_TEMPERATURE
|
||||||
|
```
|
||||||
|
|
||||||
|
The following `type` values are available:
|
||||||
|
|
||||||
|
- `CURRENT_ROOM_TEMPERATURE`
|
||||||
|
- `CURRENT_WATER_TEMPERATURE`
|
||||||
|
- `TARGET_ROOM_TEMPERATURE`
|
||||||
|
- `TARGET_WATER_TEMPERATURE`
|
||||||
|
- `HEATING_MODE`
|
||||||
|
- `ELECTRIC_POWER_LEVEL`
|
||||||
|
- `ENERGY_MIX`
|
||||||
|
- `OPERATING_STATUS`
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
The following [ESP Home actions](https://esphome.io/guides/automations.html#actions) are available:
|
||||||
|
|
||||||
|
- `truma_inetbox.heater.set_target_room_temperature`
|
||||||
|
- `temperature` - Temperature between 5C and 30C. Below 5C will disable the Heater.
|
||||||
|
- `heating_mode` - Optional set heating mode: `"OFF"`, `ECO`, `HIGH`, `BOOST`.
|
||||||
|
- `truma_inetbox.heater.set_target_water_temperature`
|
||||||
|
- `temperature` - Set water temp as number: `0`, `40`, `60`, `80`.
|
||||||
|
- `truma_inetbox.heater.set_target_water_temperature_enum`
|
||||||
|
- `temperature` - Set water temp as text: `"OFF"`, `ECO`, `HIGH`, `BOOST`.
|
||||||
|
- `truma_inetbox.heater.set_electric_power_level`
|
||||||
|
- `watt` - Set electricity level to `0`, `900`, `1800`.
|
||||||
|
- `truma_inetbox.heater.set_energy_mix`
|
||||||
|
- `energy_mix` - Set energy mix to: `GAS`, `MIX`, `ELECTRICITY`.
|
||||||
|
- `watt` - Optional: Set electricity level to `0`, `900`, `1800`
|
||||||
|
- `truma_inetbox.timer.disable` - Disable the timer configuration.
|
||||||
|
- `truma_inetbox.timer.activate` - Set a new timer configuration.
|
||||||
|
- `start` - Start time.
|
||||||
|
- `stop` - Stop time.
|
||||||
|
- `room_temperature` - Temperature between 5C and 30C.
|
||||||
|
- `heating_mode` - Optional: Set heating mode: `"OFF"`, `ECO`, `HIGH`, `BOOST`.
|
||||||
|
- `water_temperature` - Optional: Set water temp as number: `0`, `40`, `60`, `80`.
|
||||||
|
- `energy_mix` - Optional: Set energy mix to: `GAS`, `MIX`, `ELECTRICITY`.
|
||||||
|
- `watt` - Optional: Set electricity level to `0`, `900`, `1800`.
|
||||||
|
- `truma_inetbox.clock.set` - Update CP Plus from ESP Home. You *must* have another [clock source](https://esphome.io/#time-components) configured like Home Assistant Time, GPS or DS1307 RTC.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] This file
|
||||||
|
- [ ] ESP32 IDF support
|
||||||
|
- [ ] RP2040 support
|
||||||
|
- [ ] Testing of Combi 4E / Combi 6E and Alde devices (I only have access to an Combi 4)
|
||||||
|
- [ ] More Testing
|
||||||
|
|||||||
340
components/truma_inetbox/LinBusListener.cpp
Normal file
340
components/truma_inetbox/LinBusListener.cpp
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
#include "LinBusListener.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "helpers.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "soc/uart_struct.h"
|
||||||
|
#include "soc/uart_reg.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
#include "esphome/components/uart/uart_component_esp8266.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
#include "esphome/components/uart/truma_uart_component_esp32_arduino.h"
|
||||||
|
#include "esphome/components/uart/uart_component_esp32_arduino.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ESP_IDF
|
||||||
|
#include "esphome/components/uart/truma_uart_component_esp_idf.h"
|
||||||
|
#include "esphome/components/uart/uart_component_esp_idf.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
#include "esphome/components/uart/truma_uart_component_rp2040.h"
|
||||||
|
#include "esphome/components/uart/uart_component_rp2040.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
// For method `xTaskCreateUniversal`
|
||||||
|
#include <esp32-hal.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.LinBusListener";
|
||||||
|
|
||||||
|
#define LIN_BREAK 0x00
|
||||||
|
#define LIN_SYNC 0x55
|
||||||
|
#define DIAGNOSTIC_FRAME_MASTER 0x3c
|
||||||
|
#define DIAGNOSTIC_FRAME_SLAVE 0x3d
|
||||||
|
|
||||||
|
void LinBusListener::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "TODO");
|
||||||
|
|
||||||
|
this->check_uart_settings(9600, 2, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusListener::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up LIN BUS...");
|
||||||
|
this->time_per_baud_ = (1000.0f * 1000.0f / this->parent_->get_baud_rate());
|
||||||
|
this->time_per_lin_break_ = this->time_per_baud_ * this->lin_break_length * 1.1;
|
||||||
|
this->time_per_pid_ = this->time_per_baud_ * this->frame_length_ * 1.1;
|
||||||
|
this->time_per_first_byte_ = this->time_per_baud_ * this->frame_length_ * 3.0;
|
||||||
|
this->time_per_byte_ = this->time_per_baud_ * this->frame_length_ * 1.1;
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
auto uartComp = static_cast<esphome::uart::truma_ESP32ArduinoUARTComponent *>(this->parent_);
|
||||||
|
auto uart_num = uartComp->get_hw_serial_number();
|
||||||
|
|
||||||
|
// Tweak the fifo settings so data is available as soon as the first byte is recieved.
|
||||||
|
// If not it will wait either until fifo is filled or a certain time has passed.
|
||||||
|
uart_intr_config_t uart_intr;
|
||||||
|
uart_intr.intr_enable_mask =
|
||||||
|
UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M; // only these IRQs - no BREAK, PARITY or OVERFLOW
|
||||||
|
// UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M | UART_FRM_ERR_INT_ENA_M |
|
||||||
|
// UART_RXFIFO_OVF_INT_ENA_M | UART_BRK_DET_INT_ENA_M | UART_PARITY_ERR_INT_ENA_M;
|
||||||
|
uart_intr.rxfifo_full_thresh =
|
||||||
|
1; // UART_FULL_THRESH_DEFAULT, //120 default!! aghh! need receive 120 chars before we see them
|
||||||
|
uart_intr.rx_timeout_thresh =
|
||||||
|
10; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive
|
||||||
|
uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT
|
||||||
|
uart_intr_config(uart_num, &uart_intr);
|
||||||
|
#elif USE_ESP32_FRAMEWORK_ESP_IDF
|
||||||
|
|
||||||
|
// uartSetFastReading
|
||||||
|
auto uartComp = ((*uart::truma_IDFUARTComponent) this->parent_);
|
||||||
|
|
||||||
|
// Tweak the fifo settings so data is available as soon as the first byte is recieved.
|
||||||
|
// If not it will wait either until fifo is filled or a certain time has passed.
|
||||||
|
uart_intr_config_t uart_intr;
|
||||||
|
uart_intr.intr_enable_mask = 0; // UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M | UART_FRM_ERR_INT_ENA_M |
|
||||||
|
// UART_RXFIFO_OVF_INT_ENA_M | UART_BRK_DET_INT_ENA_M | UART_PARITY_ERR_INT_ENA_M;
|
||||||
|
uart_intr.rxfifo_full_thresh =
|
||||||
|
1; // UART_FULL_THRESH_DEFAULT, //120 default!! aghh! need receive 120 chars before we see them
|
||||||
|
uart_intr.rx_timeout_thresh =
|
||||||
|
1; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive
|
||||||
|
uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT
|
||||||
|
uart_intr_config(uartComp->get_hw_serial_number(), &uart_intr);
|
||||||
|
#else
|
||||||
|
// truma_RP2040UartComponent
|
||||||
|
#error Only ESP32 Arduino is supported.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
xTaskCreateUniversal(LinBusListener::read_task,
|
||||||
|
"read_task", // name
|
||||||
|
4096, // stack size (in words)
|
||||||
|
this, // input params
|
||||||
|
1, // priority
|
||||||
|
&this->read_task_handle, // handle
|
||||||
|
0 // core
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this->cs_pin_ != nullptr) {
|
||||||
|
this->cs_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusListener::write_lin_answer_(const u_int8_t *data, size_t len) {
|
||||||
|
if (!this->can_write_lin_answer_) {
|
||||||
|
ESP_LOGE(TAG, "Cannot answer LIN because there is no open order from master.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->can_write_lin_answer_ = false;
|
||||||
|
if (len > 8) {
|
||||||
|
ESP_LOGE(TAG, "LIN answer cannot be longer than 8 bytes.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t wait_time = 0;
|
||||||
|
// Was data read from FIFO and the master is awaiting an answer.
|
||||||
|
if (this->total_wait_ > 1000) {
|
||||||
|
// I am up to date and should not answer too quickly.
|
||||||
|
auto current = esp_timer_get_time();
|
||||||
|
auto wait_time_in_us = (int64_t) this->time_per_baud_ - (current - this->last_data_recieved_);
|
||||||
|
wait_time = wait_time_in_us;
|
||||||
|
if (wait_time_in_us > 1000 || wait_time_in_us < 0) {
|
||||||
|
wait_time_in_us = 0;
|
||||||
|
}
|
||||||
|
delayMicroseconds(wait_time_in_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
u_int8_t data_CRC = 0;
|
||||||
|
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_1 || this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE) {
|
||||||
|
// LIN checksum V1
|
||||||
|
data_CRC = data_checksum(data, len, 0);
|
||||||
|
} else {
|
||||||
|
// LIN checksum V2
|
||||||
|
data_CRC = data_checksum(data, len, this->current_PID_with_parity_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->observer_mode_) {
|
||||||
|
this->write_array(data, len);
|
||||||
|
this->write(data_CRC);
|
||||||
|
this->flush();
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "RESPONSE %02x %s %02x T %lli", this->current_PID_, format_hex_pretty(data, len).c_str(), data_CRC,
|
||||||
|
wait_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusListener::read_task(void *params) {
|
||||||
|
LinBusListener *instance = (LinBusListener *) params;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Check if Lin Bus is faulty.
|
||||||
|
if (instance->fault_pin_ != nullptr) {
|
||||||
|
if (!instance->fault_pin_->digital_read()) {
|
||||||
|
if (!instance->fault_on_lin_bus_reported_) {
|
||||||
|
instance->fault_on_lin_bus_reported_ = true;
|
||||||
|
ESP_LOGE(TAG, "Fault on LIN BUS detected.");
|
||||||
|
}
|
||||||
|
// Ignore any data present in buffer
|
||||||
|
instance->clear_uart_buffer_();
|
||||||
|
} else if (instance->fault_on_lin_bus_reported_) {
|
||||||
|
instance->fault_on_lin_bus_reported_ = false;
|
||||||
|
ESP_LOGI(TAG, "Fault on LIN BUS fixed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance->fault_on_lin_bus_reported_) {
|
||||||
|
while (instance->available()) {
|
||||||
|
instance->read_lin_frame_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CP Plus is inactive mode. In inactive mode it checks the bus every ~15 seconds for ~5 seconds. At the
|
||||||
|
// start it send a Break to notify devices to wake up.
|
||||||
|
auto time_since_last_activity = esp_timer_get_time() - instance->last_data_recieved_;
|
||||||
|
if (time_since_last_activity > 100 * 1000 /* 100 ms*/) {
|
||||||
|
// CP Plus is inactive.
|
||||||
|
delay(500); // NOLINT
|
||||||
|
} else {
|
||||||
|
// CP Plus is active.
|
||||||
|
// 1'000'000 ns / 9600 baud = 104 ns/baud * (8 bit + start bit + 2 stop bit) = 1144 ns/byte * 3 (BREAK,SYNC,PID) =
|
||||||
|
// ~3.5ms per preamble till I should answer. It is still working with 50ms. But thats the upper limit. CP Plus
|
||||||
|
// waits 50ms when ordering for an answer. With higher polling the number of CRC errors increases and I cannot
|
||||||
|
// answer lin orders.
|
||||||
|
delay(10); // NOLINT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusListener::read_lin_frame_() {
|
||||||
|
u_int8_t buf;
|
||||||
|
bool dataRecieved;
|
||||||
|
u_int8_t data_length, data_CRC, data_CRC_master, data_CRC_slave;
|
||||||
|
bool message_source_know, message_from_master;
|
||||||
|
|
||||||
|
// Reset current state
|
||||||
|
{
|
||||||
|
this->current_PID_with_parity_ = 0x00;
|
||||||
|
this->current_PID_ = 0x00;
|
||||||
|
this->current_data_valid = true;
|
||||||
|
this->current_data_count_ = 0;
|
||||||
|
memset(this->current_data_, 0, sizeof(this->current_data_));
|
||||||
|
this->total_wait_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First is Break expected
|
||||||
|
if (!this->read_byte(&buf) || buf != LIN_BREAK) {
|
||||||
|
// Update I recieved garbage
|
||||||
|
this->last_data_recieved_ = esp_timer_get_time();
|
||||||
|
ESP_LOGVV(TAG, "Expected BREAK not received.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update I recieved a break
|
||||||
|
this->last_data_recieved_ = esp_timer_get_time();
|
||||||
|
|
||||||
|
if (!this->wait_for_data_available_with_timeout_(this->time_per_lin_break_)) {
|
||||||
|
ESP_LOGV(TAG, "Timeout waiting for Sync");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second is Sync expected
|
||||||
|
if (!this->read_byte(&buf) || buf != LIN_SYNC) {
|
||||||
|
// No data present on UART
|
||||||
|
ESP_LOGVV(TAG, "Expected SYNC not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->wait_for_data_available_with_timeout_(this->time_per_pid_)) {
|
||||||
|
ESP_LOGVV(TAG, "Timeout waiting for PID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->read_byte(&(this->current_PID_with_parity_));
|
||||||
|
this->current_PID_ = this->current_PID_with_parity_ & 0x3F;
|
||||||
|
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_2) {
|
||||||
|
if (this->current_PID_with_parity_ != (this->current_PID_ | (addr_parity(this->current_PID_) << 6))) {
|
||||||
|
ESP_LOGW(TAG, "LIN CRC error");
|
||||||
|
this->current_data_valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->can_write_lin_answer_ = true;
|
||||||
|
// Should I response to this PID order? Ask the handling class.
|
||||||
|
this->answer_lin_order_(this->current_PID_);
|
||||||
|
this->can_write_lin_answer_ = false;
|
||||||
|
|
||||||
|
dataRecieved = wait_for_data_available_with_timeout_(this->time_per_first_byte_);
|
||||||
|
while (dataRecieved) {
|
||||||
|
this->read_byte(&buf);
|
||||||
|
if (this->current_data_count_ < sizeof(this->current_data_)) {
|
||||||
|
this->current_data_[this->current_data_count_] = buf;
|
||||||
|
this->current_data_count_++;
|
||||||
|
dataRecieved = wait_for_data_available_with_timeout_(this->time_per_byte_);
|
||||||
|
} else {
|
||||||
|
// end of data reached. There cannot be more than 9 bytes in a LIN frame.
|
||||||
|
dataRecieved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->current_data_count_ > 1) {
|
||||||
|
data_length = this->current_data_count_ - 1;
|
||||||
|
data_CRC = this->current_data_[this->current_data_count_ - 1];
|
||||||
|
message_source_know = false;
|
||||||
|
message_from_master = true;
|
||||||
|
|
||||||
|
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_1 ||
|
||||||
|
(this->current_PID_ == DIAGNOSTIC_FRAME_MASTER || this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE)) {
|
||||||
|
if (data_CRC != data_checksum(this->current_data_, data_length, 0)) {
|
||||||
|
ESP_LOGW(TAG, "LIN v1 CRC error");
|
||||||
|
this->current_data_valid = false;
|
||||||
|
}
|
||||||
|
if (this->current_PID_ == DIAGNOSTIC_FRAME_MASTER) {
|
||||||
|
message_source_know = true;
|
||||||
|
message_from_master = true;
|
||||||
|
} else if (this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE) {
|
||||||
|
message_source_know = true;
|
||||||
|
message_from_master = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data_CRC_master = data_checksum(this->current_data_, data_length, this->current_PID_);
|
||||||
|
data_CRC_slave = data_checksum(this->current_data_, data_length, this->current_PID_with_parity_);
|
||||||
|
if (data_CRC != data_CRC_master && data_CRC != data_CRC_slave) {
|
||||||
|
ESP_LOGW(TAG, "LIN v2 CRC error");
|
||||||
|
this->current_data_valid = false;
|
||||||
|
}
|
||||||
|
message_source_know = true;
|
||||||
|
if (data_CRC == data_CRC_slave) {
|
||||||
|
message_from_master = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the PID of the TRUMA Combi heater as very verbose message.
|
||||||
|
if (this->current_PID_ == 0x20 || this->current_PID_ == 0x21 || this->current_PID_ == 0x22) {
|
||||||
|
ESP_LOGVV(TAG, "PID %02x (%02x) %s %s %s", this->current_PID_, this->current_PID_with_parity_,
|
||||||
|
format_hex_pretty(this->current_data_, this->current_data_count_).c_str(),
|
||||||
|
message_source_know ? (message_from_master ? " - MASTER" : " - SLAVE") : "",
|
||||||
|
this->current_data_valid ? "" : "INVALID");
|
||||||
|
} else {
|
||||||
|
ESP_LOGV(TAG, "PID %02x (%02x) %s %s %S", this->current_PID_, this->current_PID_with_parity_,
|
||||||
|
format_hex_pretty(this->current_data_, this->current_data_count_).c_str(),
|
||||||
|
message_source_know ? (message_from_master ? " - MASTER" : " - SLAVE") : "",
|
||||||
|
this->current_data_valid ? "" : "INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->current_data_valid && message_from_master) {
|
||||||
|
this->lin_message_recieved_(this->current_PID_, this->current_data_, data_length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGV(TAG, "PID %02x (%02x) order no answer", this->current_PID_, this->current_PID_with_parity_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusListener::clear_uart_buffer_() {
|
||||||
|
u_int8_t buffer;
|
||||||
|
while (this->available() && this->read_byte(&buffer)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LinBusListener::wait_for_data_available_with_timeout_(u_int32_t timeout) {
|
||||||
|
int64_t start = esp_timer_get_time();
|
||||||
|
int64_t current = esp_timer_get_time();
|
||||||
|
int64_t latest_end = start + timeout;
|
||||||
|
while (current < latest_end) {
|
||||||
|
current = esp_timer_get_time();
|
||||||
|
if (this->available()) {
|
||||||
|
this->total_wait_ += current - start;
|
||||||
|
this->last_data_recieved_ = current;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
NOP();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
79
components/truma_inetbox/LinBusListener.h
Normal file
79
components/truma_inetbox/LinBusListener.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
enum class LIN_CHECKSUM { LIN_CHECKSUM_VERSION_1, LIN_CHECKSUM_VERSION_2 };
|
||||||
|
|
||||||
|
class LinBusListener : public PollingComponent, public uart::UARTDevice {
|
||||||
|
public:
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void set_lin_checksum(LIN_CHECKSUM val) { this->lin_checksum_ = val; }
|
||||||
|
void set_cs_pin(GPIOPin *pin) { this->cs_pin_ = pin; }
|
||||||
|
void set_fault_pin(GPIOPin *pin) { this->fault_pin_ = pin; }
|
||||||
|
void set_observer_mode(bool val) { this->observer_mode_ = val; }
|
||||||
|
bool get_lin_bus_fault() { return fault_on_lin_bus_reported_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LIN_CHECKSUM lin_checksum_;
|
||||||
|
GPIOPin *cs_pin_;
|
||||||
|
GPIOPin *fault_pin_;
|
||||||
|
bool observer_mode_ = false;
|
||||||
|
|
||||||
|
void write_lin_answer_(const u_int8_t *data, size_t len);
|
||||||
|
|
||||||
|
virtual bool answer_lin_order_(const u_int8_t pid) = 0;
|
||||||
|
virtual void lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Microseconds per UART Baud
|
||||||
|
u_int32_t time_per_baud_;
|
||||||
|
// 9.. 15
|
||||||
|
u_int8_t lin_break_length = 13;
|
||||||
|
// Microseconds per LIN Break
|
||||||
|
u_int32_t time_per_lin_break_;
|
||||||
|
u_int8_t frame_length_ = (8 /* bits */ + 1 /* Start bit */ + 2 /* Stop bits */);
|
||||||
|
// Microseconds per UART Byte (UART Frame)
|
||||||
|
u_int32_t time_per_pid_;
|
||||||
|
// Microseconds per UART Byte (UART Frame)
|
||||||
|
u_int32_t time_per_first_byte_;
|
||||||
|
// Microseconds per UART Byte (UART Frame)
|
||||||
|
u_int32_t time_per_byte_;
|
||||||
|
|
||||||
|
bool fault_on_lin_bus_reported_ = false;
|
||||||
|
bool can_write_lin_answer_ = false;
|
||||||
|
|
||||||
|
u_int8_t current_PID_with_parity_ = 0x00;
|
||||||
|
u_int8_t current_PID_ = 0x00;
|
||||||
|
bool current_data_valid = true;
|
||||||
|
u_int8_t current_data_count_ = 0;
|
||||||
|
// up to 8 byte data frame + CRC
|
||||||
|
u_int8_t current_data_[9] = {};
|
||||||
|
// Total wait time for this LIN Frame (Break, SYNC, Data, CRC)
|
||||||
|
u_int32_t total_wait_;
|
||||||
|
// Time when the last LIN data was available.
|
||||||
|
int64_t last_data_recieved_;
|
||||||
|
|
||||||
|
TaskHandle_t read_task_handle = NULL;
|
||||||
|
static void read_task(void *params);
|
||||||
|
|
||||||
|
void read_lin_frame_();
|
||||||
|
void clear_uart_buffer_();
|
||||||
|
bool wait_for_data_available_with_timeout_(u_int32_t timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
254
components/truma_inetbox/LinBusProtocol.cpp
Normal file
254
components/truma_inetbox/LinBusProtocol.cpp
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
#include "LinBusProtocol.h"
|
||||||
|
#include <array>
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.LinBusProtocol";
|
||||||
|
|
||||||
|
#define DIAGNOSTIC_FRAME_MASTER 0x3c
|
||||||
|
#define DIAGNOSTIC_FRAME_SLAVE 0x3d
|
||||||
|
#define LIN_NAD_BROADCAST 0x7F
|
||||||
|
#define LIN_SID_RESPONSE 0x40
|
||||||
|
#define LIN_SID_ASSIGN_NAD 0xB0
|
||||||
|
#define LIN_SID_ASSIGN_NAD_RESPONSE (LIN_SID_ASSIGN_NAD | LIN_SID_RESPONSE)
|
||||||
|
#define LIN_SID_READ_BY_IDENTIFIER 0xB2
|
||||||
|
#define LIN_SID_READ_BY_IDENTIFIER_RESPONSE (LIN_SID_READ_BY_IDENTIFIER | LIN_SID_RESPONSE)
|
||||||
|
#define LIN_SID_HEARTBEAT 0xB9
|
||||||
|
#define LIN_SID_HEARTBEAT_RESPONSE (LIN_SID_HEARTBEAT | LIN_SID_RESPONSE)
|
||||||
|
#define LIN_SID_READ_STATE_BUFFER 0xBA
|
||||||
|
#define LIN_SID_READ_STATE_BUFFER_RESPONSE (LIN_SID_READ_STATE_BUFFER | LIN_SID_RESPONSE)
|
||||||
|
#define LIN_SID_FIll_STATE_BUFFFER 0xBB
|
||||||
|
#define LIN_SID_FIll_STATE_BUFFFER_BRESPONSE (LIN_SID_FIll_STATE_BUFFFER | LIN_SID_RESPONSE)
|
||||||
|
|
||||||
|
bool LinBusProtocol::answer_lin_order_(const u_int8_t pid) {
|
||||||
|
// Send requested answer
|
||||||
|
if (pid == DIAGNOSTIC_FRAME_SLAVE) {
|
||||||
|
if (!this->updates_to_send_.empty()) {
|
||||||
|
auto update_to_send_ = this->updates_to_send_.front();
|
||||||
|
this->updates_to_send_.pop();
|
||||||
|
this->write_lin_answer_(update_to_send_.data(), update_to_send_.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusProtocol::lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) {
|
||||||
|
if (pid == DIAGNOSTIC_FRAME_MASTER) {
|
||||||
|
// The original Inet Box is answering this message. Works fine without.
|
||||||
|
// std::array<u_int8_t, 8> message_array = {};
|
||||||
|
// std::copy(message, message + length, message_array.begin());
|
||||||
|
// if (message_array == this->lin_empty_response_) {
|
||||||
|
// std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
// response[0] = 0x00;
|
||||||
|
// response[1] = 0x55;
|
||||||
|
// response[2] = 0x03; // this->lin_node_address_;
|
||||||
|
// response[3] = 0x66;
|
||||||
|
// response[4] = 0x5B;
|
||||||
|
// response[5] = 0xA7;
|
||||||
|
// response[6] = 0x0E;
|
||||||
|
// response[7] = 0x49;
|
||||||
|
// this->prepare_update_msg_(response);
|
||||||
|
// }
|
||||||
|
|
||||||
|
this->lin_message_recieved_diagnostic_(message, length);
|
||||||
|
|
||||||
|
} else if (pid == this->lin_node_address_) {
|
||||||
|
ESP_LOGW(TAG, "Unhandled message for me.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusProtocol::prepare_update_msg_(const std::array<u_int8_t, 8> message) {
|
||||||
|
this->updates_to_send_.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LinBusProtocol::is_matching_identifier_(const u_int8_t *message) {
|
||||||
|
auto lin_identifier = this->lin_identifier();
|
||||||
|
return message[0] == lin_identifier[0] && message[1] == lin_identifier[1] && message[2] == lin_identifier[2] &&
|
||||||
|
message[3] == lin_identifier[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusProtocol::lin_message_recieved_diagnostic_(const u_int8_t *message, u_int8_t length) {
|
||||||
|
u_int8_t node_address = message[0];
|
||||||
|
bool my_node_address = node_address == this->lin_node_address_;
|
||||||
|
bool broadcast_address = node_address == LIN_NAD_BROADCAST;
|
||||||
|
if (!my_node_address && !broadcast_address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
u_int8_t protocol_control_information = message[1];
|
||||||
|
u_int16_t message_length = 0;
|
||||||
|
u_int8_t service_identifier = 0;
|
||||||
|
if ((protocol_control_information & 0xF0) == 0x00) {
|
||||||
|
// Single Frame mode
|
||||||
|
{
|
||||||
|
// End any open Multi frame mode message
|
||||||
|
this->multi_pdu_message_expected_size_ = 0;
|
||||||
|
this->multi_pdu_message_len_ = 0;
|
||||||
|
}
|
||||||
|
message_length = protocol_control_information;
|
||||||
|
service_identifier = message[2];
|
||||||
|
if (message_length > 6) {
|
||||||
|
ESP_LOGE(TAG, "LIN Protocol issue: Single frame message too long.");
|
||||||
|
// ignore invalid message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ((protocol_control_information & 0xF0) == 0x10) {
|
||||||
|
// First Frame of multi PDU message
|
||||||
|
message_length = (protocol_control_information & 0x0F << 8) + message[2];
|
||||||
|
service_identifier = message[3];
|
||||||
|
if (message_length < 7) {
|
||||||
|
ESP_LOGE(TAG, "LIN Protocol issue: Multi frame message too short.");
|
||||||
|
// ignore invalid message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message_length > sizeof(this->multi_pdu_message_)) {
|
||||||
|
ESP_LOGE(TAG, "LIN Protocol issue: Multi frame message too long.");
|
||||||
|
// ignore invalid message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->multi_pdu_message_expected_size_ = message_length;
|
||||||
|
this->multi_pdu_message_len_ = 0;
|
||||||
|
for (size_t i = 3; i < 8; i++) {
|
||||||
|
this->multi_pdu_message_[this->multi_pdu_message_len_++] = message[i];
|
||||||
|
}
|
||||||
|
// Message is handeld
|
||||||
|
return;
|
||||||
|
} else if ((protocol_control_information & 0xF0) == 0x20) {
|
||||||
|
// Consecutive Frames
|
||||||
|
if (this->multi_pdu_message_len_ >= this->multi_pdu_message_expected_size_) {
|
||||||
|
// ignore, because i don't await a consecutive frame
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->lin_message_recieved_diagnostic_multi_(message, length, protocol_control_information);
|
||||||
|
// Message is handeld
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service_identifier == LIN_SID_READ_BY_IDENTIFIER && message_length == 6) {
|
||||||
|
if (this->is_matching_identifier_(&message[4])) {
|
||||||
|
// I have observed the following identifiers:
|
||||||
|
// broadcast_address:
|
||||||
|
// - 0x00 - response lin_identifier[0:4] + 0x00 /* Hardware revision*/
|
||||||
|
// my_node_address:
|
||||||
|
// - 0x20 - displayed version
|
||||||
|
// - 0x22 - unkown
|
||||||
|
auto identifier = message[3];
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
|
||||||
|
std::array<u_int8_t, 5> identifier_response = {};
|
||||||
|
if (this->lin_read_field_by_identifier_(identifier, &identifier_response)) {
|
||||||
|
response[1] = 6; /* bytes length - ignored by CP Plus?*/
|
||||||
|
response[2] = LIN_SID_READ_BY_IDENTIFIER_RESPONSE;
|
||||||
|
auto iterator = response.begin();
|
||||||
|
std::advance(iterator, 3);
|
||||||
|
std::copy(identifier_response.data(), identifier_response.data() + identifier_response.size(), iterator);
|
||||||
|
} else {
|
||||||
|
// Not supported - Negative response (see 4.2.6.1 Read by identifier)
|
||||||
|
response[1] = 3; /* bytes length*/
|
||||||
|
response[2] = 0x7F;
|
||||||
|
response[3] = LIN_SID_READ_BY_IDENTIFIER;
|
||||||
|
response[4] = 0x12;
|
||||||
|
}
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
}
|
||||||
|
} else if (my_node_address && service_identifier == LIN_SID_HEARTBEAT && message_length >= 5) {
|
||||||
|
// if (message[3] == 0x00 && message[4] == 0x1F && message[5] == 0x00 && message[6] == 0x00) {
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
response[1] = 2; /* bytes length*/
|
||||||
|
response[2] = LIN_SID_HEARTBEAT_RESPONSE;
|
||||||
|
response[3] = 0x00;
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
//}
|
||||||
|
} else if (broadcast_address && service_identifier == LIN_SID_ASSIGN_NAD && message_length == 6) {
|
||||||
|
if (this->is_matching_identifier_(&message[3])) {
|
||||||
|
ESP_LOGI(TAG, "Assigned new SID %02x and reset device", message[7]);
|
||||||
|
|
||||||
|
// send response with old node address.
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
response[1] = 1; /* bytes length*/
|
||||||
|
response[2] = LIN_SID_ASSIGN_NAD_RESPONSE;
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
this->lin_node_address_ = message[7];
|
||||||
|
|
||||||
|
// assumption an assign new SID occurs as part of init process.
|
||||||
|
this->lin_reset_device();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (my_node_address) {
|
||||||
|
ESP_LOGD(TAG, "SID %02x MY - %s - Unhandled", service_identifier, format_hex_pretty(message, length).c_str());
|
||||||
|
} else if (broadcast_address) {
|
||||||
|
ESP_LOGD(TAG, "SID %02x BC - %s - Unhandled", service_identifier, format_hex_pretty(message, length).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusProtocol::lin_message_recieved_diagnostic_multi_(const u_int8_t *message, u_int8_t length,
|
||||||
|
u_int8_t protocol_control_information) {
|
||||||
|
u_int8_t frame_counter = protocol_control_information - 0x21;
|
||||||
|
for (size_t i = 2; i < 8; i++) {
|
||||||
|
if (this->multi_pdu_message_len_ < this->multi_pdu_message_expected_size_) {
|
||||||
|
this->multi_pdu_message_[this->multi_pdu_message_len_++] = message[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this->multi_pdu_message_len_ == this->multi_pdu_message_expected_size_) {
|
||||||
|
ESP_LOGD(TAG, "Multi package request %s",
|
||||||
|
format_hex_pretty(this->multi_pdu_message_, this->multi_pdu_message_len_).c_str());
|
||||||
|
|
||||||
|
u_int8_t answer_len = 0;
|
||||||
|
auto answer = this->lin_multiframe_recieved(this->multi_pdu_message_, this->multi_pdu_message_len_, &answer_len);
|
||||||
|
if (answer_len > 0) {
|
||||||
|
ESP_LOGD(TAG, "Multi package response %s", format_hex_pretty(answer, answer_len).c_str());
|
||||||
|
|
||||||
|
if (answer_len <= 6) {
|
||||||
|
// Single Frame response
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
response[1] = answer_len; /* bytes length */
|
||||||
|
response[2] = answer[0] | LIN_SID_RESPONSE;
|
||||||
|
for (size_t i = 1; i < answer_len; i++) {
|
||||||
|
response[i + 2] = answer[i];
|
||||||
|
}
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
} else {
|
||||||
|
// Multi Frame response
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
response[1] = 0x10 | ((answer_len >> 8) & 0x0F);
|
||||||
|
response[2] = answer_len & 0xFF;
|
||||||
|
response[3] = answer[0] | LIN_SID_RESPONSE;
|
||||||
|
for (size_t i = 1; i < 5; i++) {
|
||||||
|
response[i + 3] = answer[i];
|
||||||
|
}
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
|
||||||
|
u_int16_t answer_position = 5; // The first 5 bytes are sent in First frame of multi frame response.
|
||||||
|
u_int8_t answer_frame_counter = 0; // Each answer frame can contain 6 bytes
|
||||||
|
while (answer_position < answer_len) {
|
||||||
|
response = this->lin_empty_response_;
|
||||||
|
response[0] = this->lin_node_address_;
|
||||||
|
response[1] = ((answer_frame_counter + 1) & 0x0F) | 0x20;
|
||||||
|
for (size_t i = 0; i < 6; i++) {
|
||||||
|
if (answer_position < answer_len) {
|
||||||
|
response[i + 2] = answer[answer_position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->prepare_update_msg_(response);
|
||||||
|
answer_frame_counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LinBusProtocol::lin_message_recieved_diagnostic_single_(const u_int8_t *message, u_int8_t length) {
|
||||||
|
// TODO: Split up `lin_message_recieved_diagnostic_` method.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
42
components/truma_inetbox/LinBusProtocol.h
Normal file
42
components/truma_inetbox/LinBusProtocol.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
#include "LinBusListener.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
class LinBusProtocol : public LinBusListener {
|
||||||
|
public:
|
||||||
|
virtual const std::array<u_int8_t, 4> lin_identifier() = 0;
|
||||||
|
virtual void lin_reset_device() = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const std::array<u_int8_t, 8> lin_empty_response_ = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
|
bool answer_lin_order_(const u_int8_t pid) override;
|
||||||
|
void lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) override;
|
||||||
|
|
||||||
|
virtual bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) = 0;
|
||||||
|
virtual const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
|
||||||
|
u_int8_t *return_len) = 0;
|
||||||
|
|
||||||
|
std::queue<std::array<u_int8_t, 8>> updates_to_send_ = {};
|
||||||
|
|
||||||
|
private:
|
||||||
|
u_int8_t lin_node_address_ = /*LIN initial node address*/ 0x03;
|
||||||
|
|
||||||
|
void prepare_update_msg_(const std::array<u_int8_t, 8> message);
|
||||||
|
bool is_matching_identifier_(const u_int8_t *message);
|
||||||
|
|
||||||
|
u_int16_t multi_pdu_message_expected_size_ = 0;
|
||||||
|
u_int8_t multi_pdu_message_len_ = 0;
|
||||||
|
u_int8_t multi_pdu_message_[64];
|
||||||
|
void lin_message_recieved_diagnostic_(const u_int8_t *message, u_int8_t length);
|
||||||
|
void lin_message_recieved_diagnostic_multi_(const u_int8_t *message, u_int8_t length,
|
||||||
|
u_int8_t protocol_control_information);
|
||||||
|
void lin_message_recieved_diagnostic_single_(const u_int8_t *message, u_int8_t length);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
86
components/truma_inetbox/TrumaStatusFrame.cpp
Normal file
86
components/truma_inetbox/TrumaStatusFrame.cpp
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#include "TrumaStatusFrame.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
#include "helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
void status_frame_create_empty(StatusFrame *response, u_int8_t message_type, u_int8_t message_length,
|
||||||
|
u_int8_t command_counter) {
|
||||||
|
response->inner.genericHeader.service_identifier = LIN_SID_READ_STATE_BUFFER | LIN_SID_RESPONSE;
|
||||||
|
// Copy header over for this message.
|
||||||
|
for (size_t i = 1; i < truma_message_header.size(); i++) {
|
||||||
|
response->raw[i] = truma_message_header[i];
|
||||||
|
}
|
||||||
|
response->inner.genericHeader.header_2 = 'T';
|
||||||
|
response->inner.genericHeader.header_3 = 0x01;
|
||||||
|
response->inner.genericHeader.message_type = message_type;
|
||||||
|
response->inner.genericHeader.message_length = message_length;
|
||||||
|
response->inner.genericHeader.command_counter = command_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void status_frame_calculate_checksum(StatusFrame *response) {
|
||||||
|
response->inner.genericHeader.checksum = 0x0;
|
||||||
|
response->inner.genericHeader.checksum = data_checksum(&response->raw[10], sizeof(StatusFrame) - 10, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void status_frame_create_init(StatusFrame *response, u_int8_t command_counter) {
|
||||||
|
status_frame_create_empty(response, STATUS_FRAME_RESPONSE_INIT_REQUEST, 0, command_counter);
|
||||||
|
|
||||||
|
status_frame_calculate_checksum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void status_frame_create_update_clock(StatusFrame *response, u_int8_t command_counter, u_int8_t hour, u_int8_t minute,
|
||||||
|
u_int8_t second, ClockMode clockMode) {
|
||||||
|
status_frame_create_empty(response, STATUS_FRAME_CLOCK_RESPONSE, sizeof(StatusFrameClock), command_counter);
|
||||||
|
|
||||||
|
response->inner.clock.clock_hour = hour;
|
||||||
|
response->inner.clock.clock_minute = minute;
|
||||||
|
response->inner.clock.clock_second = second;
|
||||||
|
response->inner.clock.display_1 = 0x1;
|
||||||
|
response->inner.clock.display_2 = 0x1;
|
||||||
|
response->inner.clock.clock_mode = clockMode;
|
||||||
|
|
||||||
|
status_frame_calculate_checksum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void status_frame_create_update_timer(StatusFrame *response, u_int8_t command_counter, TimerActive active,
|
||||||
|
u_int8_t start_hour, u_int8_t start_minute, u_int8_t stop_hour,
|
||||||
|
u_int8_t stop_minute, TargetTemp room, TargetTemp water, HeatingMode mode,
|
||||||
|
EnergyMix energy, ElectricPowerLevel elPower) {
|
||||||
|
status_frame_create_empty(response, STATUS_FRAME_TIMER_RESPONSE, sizeof(StatusFrameTimerResponse), command_counter);
|
||||||
|
|
||||||
|
response->inner.timerResponse.timer_target_temp_room = room;
|
||||||
|
response->inner.timerResponse.timer_heating_mode = mode;
|
||||||
|
response->inner.timerResponse.timer_target_temp_water = water;
|
||||||
|
response->inner.timerResponse.timer_energy_mix_a = energy;
|
||||||
|
response->inner.timerResponse.timer_energy_mix_b = energy;
|
||||||
|
response->inner.timerResponse.timer_el_power_level_a = elPower;
|
||||||
|
response->inner.timerResponse.timer_el_power_level_b = elPower;
|
||||||
|
response->inner.timerResponse.timer_resp_active = active;
|
||||||
|
response->inner.timerResponse.timer_resp_start_hours = start_hour;
|
||||||
|
response->inner.timerResponse.timer_resp_start_minutes = start_minute;
|
||||||
|
response->inner.timerResponse.timer_resp_stop_hours = stop_hour;
|
||||||
|
response->inner.timerResponse.timer_resp_stop_minutes = stop_minute;
|
||||||
|
|
||||||
|
status_frame_calculate_checksum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void status_frame_create_update_heater(StatusFrame *response, u_int8_t command_counter, TargetTemp room,
|
||||||
|
TargetTemp water, HeatingMode mode, EnergyMix energy,
|
||||||
|
ElectricPowerLevel elPower) {
|
||||||
|
status_frame_create_empty(response, STATUS_FRAME_HEATER_RESPONSE, sizeof(StatusFrameHeaterResponse), command_counter);
|
||||||
|
|
||||||
|
response->inner.heaterResponse.target_temp_room = room;
|
||||||
|
response->inner.heaterResponse.heating_mode = mode;
|
||||||
|
response->inner.heaterResponse.target_temp_water = water;
|
||||||
|
response->inner.heaterResponse.energy_mix_a = energy;
|
||||||
|
response->inner.heaterResponse.energy_mix_b = energy;
|
||||||
|
response->inner.heaterResponse.el_power_level_a = elPower;
|
||||||
|
response->inner.heaterResponse.el_power_level_b = elPower;
|
||||||
|
|
||||||
|
status_frame_calculate_checksum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
28
components/truma_inetbox/TrumaStatusFrame.h
Normal file
28
components/truma_inetbox/TrumaStatusFrame.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
void status_frame_create_empty(StatusFrame *response, u_int8_t message_type, u_int8_t message_length,
|
||||||
|
u_int8_t command_counter);
|
||||||
|
|
||||||
|
void status_frame_calculate_checksum(StatusFrame *response);
|
||||||
|
|
||||||
|
void status_frame_create_init(StatusFrame *response, u_int8_t command_counter);
|
||||||
|
|
||||||
|
void status_frame_create_update_clock(StatusFrame *response, u_int8_t command_counter, u_int8_t hour, u_int8_t minute,
|
||||||
|
u_int8_t second, ClockMode clockMode);
|
||||||
|
|
||||||
|
void status_frame_create_update_timer(StatusFrame *response, u_int8_t command_counter, TimerActive active,
|
||||||
|
u_int8_t start_hour, u_int8_t start_minute, u_int8_t stop_hour,
|
||||||
|
u_int8_t stop_minute, TargetTemp room, TargetTemp water, HeatingMode mode,
|
||||||
|
EnergyMix energy, ElectricPowerLevel elPower);
|
||||||
|
|
||||||
|
void status_frame_create_update_heater(StatusFrame *response, u_int8_t command_counter, TargetTemp room,
|
||||||
|
TargetTemp water, HeatingMode mode, EnergyMix energy,
|
||||||
|
ElectricPowerLevel elPower);
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
439
components/truma_inetbox/TrumaiNetBoxApp.cpp
Normal file
439
components/truma_inetbox/TrumaiNetBoxApp.cpp
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
#include "TrumaStatusFrame.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.TrumaiNetBoxApp";
|
||||||
|
|
||||||
|
TrumaiNetBoxApp::TrumaiNetBoxApp(u_int8_t expected_listener_count) {
|
||||||
|
this->listeners_heater_.reserve(expected_listener_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaiNetBoxApp::update() {
|
||||||
|
// Call listeners in after method 'lin_multiframe_recieved' call.
|
||||||
|
// Because 'lin_multiframe_recieved' is time critical an all these sensors can take some time.
|
||||||
|
if (this->status_heater_updated_ || this->status_timer_updated_ || this->status_clock_updated_ ||
|
||||||
|
this->status_config_updated_) {
|
||||||
|
// Run through listeners
|
||||||
|
for (auto &listener : this->listeners_heater_) {
|
||||||
|
if (this->status_heater_updated_ && listener.on_heater_change != nullptr) {
|
||||||
|
listener.on_heater_change(&this->status_heater_);
|
||||||
|
}
|
||||||
|
if (this->status_timer_updated_ && listener.on_timer_change != nullptr) {
|
||||||
|
listener.on_timer_change(&this->status_timer_);
|
||||||
|
}
|
||||||
|
if (this->status_clock_updated_ && listener.on_clock_change != nullptr) {
|
||||||
|
listener.on_clock_change(&this->status_clock_);
|
||||||
|
}
|
||||||
|
if (this->status_config_updated_ && listener.on_config_change != nullptr) {
|
||||||
|
listener.on_config_change(&this->status_config_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run through callbacks
|
||||||
|
if (this->status_heater_updated_) {
|
||||||
|
this->state_heater_callback_.call(&this->status_heater_);
|
||||||
|
}
|
||||||
|
// update is handeld
|
||||||
|
this->status_heater_updated_ = false;
|
||||||
|
this->status_timer_updated_ = false;
|
||||||
|
this->status_clock_updated_ = false;
|
||||||
|
this->status_config_updated_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array<uint8_t, 4> TrumaiNetBoxApp::lin_identifier() {
|
||||||
|
// Unkown:
|
||||||
|
// 17.46.01.03 - Unkown more comms required for init.
|
||||||
|
// 17.46.10.03 - Unkown more comms required for init.
|
||||||
|
// Heater:
|
||||||
|
// 17.46.40.03 - H2.00.01 - 0340.xx Combi 4/6
|
||||||
|
// Aircon:
|
||||||
|
// 17.46.00.0C - A23.70.0 - 0C00.xx (with light option: OFF/1..5)
|
||||||
|
// 17.46.01.0C - A23.70.0 - 0C01.xx
|
||||||
|
// 17.46.04.0C - A23.70.0 - 0C04.xx (with light option: OFF/1..5)
|
||||||
|
// 17.46.05.0C - A23.70.0 - 0C05.xx
|
||||||
|
// 17.46.06.0C - A23.70.0 - 0C06.xx (with light option: OFF/1..5)
|
||||||
|
// 17.46.07.0C - A23.70.0 - 0C07.xx (with light option: OFF/1..5)
|
||||||
|
// iNet Box:
|
||||||
|
// 17.46.00.1F - T23.70.0 - 1F00.xx iNet Box
|
||||||
|
return {0x17 /*Supplied Id*/, 0x46 /*Supplied Id*/, 0x00 /*Function Id*/, 0x1F /*Function Id*/};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaiNetBoxApp::lin_reset_device() {
|
||||||
|
this->device_registered_ = esp_timer_get_time();
|
||||||
|
this->init_recieved_ = 0;
|
||||||
|
|
||||||
|
this->status_heater_valid_ = false;
|
||||||
|
this->status_heater_updated_ = false;
|
||||||
|
this->status_timer_valid_ = false;
|
||||||
|
this->status_timer_updated_ = false;
|
||||||
|
this->status_clock_valid_ = false;
|
||||||
|
this->status_clock_updated_ = false;
|
||||||
|
this->status_config_valid_ = false;
|
||||||
|
this->status_config_updated_ = false;
|
||||||
|
|
||||||
|
this->update_time_ = 0;
|
||||||
|
|
||||||
|
this->update_status_heater_prepared_ = false;
|
||||||
|
this->update_status_heater_unsubmitted_ = false;
|
||||||
|
this->update_status_heater_stale_ = false;
|
||||||
|
|
||||||
|
this->update_status_timer_prepared_ = false;
|
||||||
|
this->update_status_timer_unsubmitted_ = false;
|
||||||
|
this->update_status_timer_stale_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameHeater *)> &func) {
|
||||||
|
auto listener = StatusFrameListener{
|
||||||
|
.on_heater_change = func,
|
||||||
|
};
|
||||||
|
this->listeners_heater_.push_back(std::move(listener));
|
||||||
|
|
||||||
|
if (this->status_heater_valid_) {
|
||||||
|
func(&this->status_heater_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameTimer *)> &func) {
|
||||||
|
auto listener = StatusFrameListener{
|
||||||
|
.on_timer_change = func,
|
||||||
|
};
|
||||||
|
this->listeners_heater_.push_back(std::move(listener));
|
||||||
|
|
||||||
|
if (this->status_timer_valid_) {
|
||||||
|
func(&this->status_timer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameClock *)> &func) {
|
||||||
|
auto listener = StatusFrameListener{
|
||||||
|
.on_clock_change = func,
|
||||||
|
};
|
||||||
|
this->listeners_heater_.push_back(std::move(listener));
|
||||||
|
|
||||||
|
if (this->status_clock_valid_) {
|
||||||
|
func(&this->status_clock_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameConfig *)> &func) {
|
||||||
|
auto listener = StatusFrameListener{
|
||||||
|
.on_config_change = func,
|
||||||
|
};
|
||||||
|
this->listeners_heater_.push_back(std::move(listener));
|
||||||
|
|
||||||
|
if (this->status_config_valid_) {
|
||||||
|
func(&this->status_config_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusFrameHeaterResponse *TrumaiNetBoxApp::update_heater_prepare() {
|
||||||
|
// An update is currently going on.
|
||||||
|
if (this->update_status_heater_prepared_ || this->update_status_heater_stale_) {
|
||||||
|
return &this->update_status_heater_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare status heater response
|
||||||
|
this->update_status_heater_ = {};
|
||||||
|
this->update_status_heater_.target_temp_room = this->status_heater_.target_temp_room;
|
||||||
|
this->update_status_heater_.heating_mode = this->status_heater_.heating_mode;
|
||||||
|
this->update_status_heater_.el_power_level_a = this->status_heater_.el_power_level_a;
|
||||||
|
this->update_status_heater_.target_temp_water = this->status_heater_.target_temp_water;
|
||||||
|
this->update_status_heater_.el_power_level_b = this->status_heater_.el_power_level_b;
|
||||||
|
this->update_status_heater_.energy_mix_a = this->status_heater_.energy_mix_a;
|
||||||
|
this->update_status_heater_.energy_mix_b = this->status_heater_.energy_mix_b;
|
||||||
|
|
||||||
|
this->update_status_heater_prepared_ = true;
|
||||||
|
return &this->update_status_heater_;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusFrameTimerResponse *TrumaiNetBoxApp::update_timer_prepare() {
|
||||||
|
// An update is currently going on.
|
||||||
|
if (this->update_status_timer_prepared_ || this->update_status_timer_stale_) {
|
||||||
|
return &this->update_status_timer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare status heater response
|
||||||
|
this->update_status_timer_ = {};
|
||||||
|
this->update_status_timer_.timer_target_temp_room = this->status_timer_.timer_target_temp_room;
|
||||||
|
this->update_status_timer_.timer_heating_mode = this->status_timer_.timer_heating_mode;
|
||||||
|
this->update_status_timer_.timer_el_power_level_a = this->status_timer_.timer_el_power_level_a;
|
||||||
|
this->update_status_timer_.timer_target_temp_water = this->status_timer_.timer_target_temp_water;
|
||||||
|
this->update_status_timer_.timer_el_power_level_b = this->status_timer_.timer_el_power_level_b;
|
||||||
|
this->update_status_timer_.timer_energy_mix_a = this->status_timer_.timer_energy_mix_a;
|
||||||
|
this->update_status_timer_.timer_energy_mix_b = this->status_timer_.timer_energy_mix_b;
|
||||||
|
this->update_status_timer_.timer_resp_active = this->status_timer_.timer_active;
|
||||||
|
this->update_status_timer_.timer_resp_start_minutes = this->status_timer_.timer_start_minutes;
|
||||||
|
this->update_status_timer_.timer_resp_start_hours = this->status_timer_.timer_start_hours;
|
||||||
|
this->update_status_timer_.timer_resp_stop_minutes = this->status_timer_.timer_stop_minutes;
|
||||||
|
this->update_status_timer_.timer_resp_stop_hours = this->status_timer_.timer_stop_hours;
|
||||||
|
|
||||||
|
this->update_status_timer_prepared_ = true;
|
||||||
|
return &this->update_status_timer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::answer_lin_order_(const u_int8_t pid) {
|
||||||
|
// Alive message
|
||||||
|
if (pid == LIN_PID_TRUMA_INET_BOX) {
|
||||||
|
std::array<u_int8_t, 8> response = this->lin_empty_response_;
|
||||||
|
|
||||||
|
if (this->updates_to_send_.empty() && !this->has_update_to_submit_()) {
|
||||||
|
response[0] = 0xFE;
|
||||||
|
}
|
||||||
|
this->write_lin_answer_(response.data(), sizeof(response));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return LinBusProtocol::answer_lin_order_(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) {
|
||||||
|
this->device_registered_ = esp_timer_get_time();
|
||||||
|
if (identifier == 0x00 /* LIN Product Identification */) {
|
||||||
|
auto lin_identifier = this->lin_identifier();
|
||||||
|
(*response)[0] = lin_identifier[0];
|
||||||
|
(*response)[1] = lin_identifier[1];
|
||||||
|
(*response)[2] = lin_identifier[2];
|
||||||
|
(*response)[3] = lin_identifier[3];
|
||||||
|
(*response)[4] = 0x01; // Hardware revision
|
||||||
|
return true;
|
||||||
|
} else if (identifier == 0x20 /* Product details to display in CP plus */) {
|
||||||
|
auto lin_identifier = this->lin_identifier();
|
||||||
|
// Only the first three parts are used.
|
||||||
|
(*response)[0] = lin_identifier[0];
|
||||||
|
(*response)[1] = lin_identifier[1];
|
||||||
|
(*response)[2] = lin_identifier[2];
|
||||||
|
// (*response)[3] = // unkown
|
||||||
|
// (*response)[4] = // unkown
|
||||||
|
return true;
|
||||||
|
} else if (identifier == 0x22 /* unkown usage */) {
|
||||||
|
// Init is failing if missing
|
||||||
|
// Data can be anything?
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u_int8_t *TrumaiNetBoxApp::lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
|
||||||
|
u_int8_t *return_len) {
|
||||||
|
static u_int8_t response[48] = {};
|
||||||
|
// Validate message prefix.
|
||||||
|
if (message_len < truma_message_header.size()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (u_int8_t i = 1; i < truma_message_header.size(); i++) {
|
||||||
|
if (message[i] != truma_message_header[i]) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message[0] == LIN_SID_READ_STATE_BUFFER) {
|
||||||
|
// Example: BA.00.1F.00.1E.00.00.22.FF.FF.FF (11)
|
||||||
|
memset(response, 0, sizeof(response));
|
||||||
|
auto response_frame = reinterpret_cast<StatusFrame *>(response);
|
||||||
|
|
||||||
|
// The order must match with the method 'has_update_to_submit_'.
|
||||||
|
if (this->init_recieved_ == 0) {
|
||||||
|
status_frame_create_init(response_frame, this->message_counter++);
|
||||||
|
// TODO: can I create a shorter (quicker) messsage here?
|
||||||
|
(*return_len) = sizeof(StatusFrame);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (this->update_status_heater_unsubmitted_) {
|
||||||
|
status_frame_create_update_heater(
|
||||||
|
response_frame, this->message_counter++, this->update_status_heater_.target_temp_room,
|
||||||
|
this->update_status_heater_.target_temp_water, this->update_status_heater_.heating_mode,
|
||||||
|
this->update_status_heater_.energy_mix_a, this->update_status_heater_.el_power_level_a);
|
||||||
|
|
||||||
|
this->update_time_ = 0;
|
||||||
|
this->update_status_heater_prepared_ = false;
|
||||||
|
this->update_status_heater_unsubmitted_ = false;
|
||||||
|
this->update_status_heater_stale_ = true;
|
||||||
|
// Remove last 12 bytes (2 Frames), because they are always 0.
|
||||||
|
// This cannot be done on the first message, but later messages it is fine.
|
||||||
|
(*return_len) = sizeof(StatusFrame);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (this->update_status_timer_unsubmitted_) {
|
||||||
|
status_frame_create_update_timer(
|
||||||
|
response_frame, this->message_counter++, this->update_status_timer_.timer_resp_active,
|
||||||
|
this->update_status_timer_.timer_resp_start_hours, this->update_status_timer_.timer_resp_start_minutes,
|
||||||
|
this->update_status_timer_.timer_resp_stop_hours, this->update_status_timer_.timer_resp_stop_minutes,
|
||||||
|
this->update_status_timer_.timer_target_temp_room, this->update_status_timer_.timer_target_temp_water,
|
||||||
|
this->update_status_timer_.timer_heating_mode, this->update_status_timer_.timer_energy_mix_a,
|
||||||
|
this->update_status_timer_.timer_el_power_level_a);
|
||||||
|
|
||||||
|
this->update_time_ = 0;
|
||||||
|
this->update_status_timer_prepared_ = false;
|
||||||
|
this->update_status_timer_unsubmitted_ = false;
|
||||||
|
this->update_status_timer_stale_ = true;
|
||||||
|
(*return_len) = sizeof(StatusFrame);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (this->update_status_clock_unsubmitted_) {
|
||||||
|
// read time live
|
||||||
|
auto now = this->time_->now();
|
||||||
|
|
||||||
|
status_frame_create_update_clock(response_frame, this->message_counter++, now.hour, now.minute, now.second,
|
||||||
|
this->status_clock_.clock_mode);
|
||||||
|
|
||||||
|
this->update_status_clock_unsubmitted_ = false;
|
||||||
|
(*return_len) = sizeof(StatusFrame);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message_len < sizeof(StatusFrame) && message[0] == LIN_SID_FIll_STATE_BUFFFER) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto statusFrame = reinterpret_cast<const StatusFrame *>(message);
|
||||||
|
auto header = &statusFrame->inner.genericHeader;
|
||||||
|
// Validate Truma frame checksum
|
||||||
|
if (header->checksum != data_checksum(&statusFrame->raw[10], sizeof(StatusFrame) - 10, (0xFF - header->checksum)) ||
|
||||||
|
header->header_2 != 'T' || header->header_3 != 0x01) {
|
||||||
|
ESP_LOGE(TAG, "Truma checksum fail.");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create acknowledge response.
|
||||||
|
response[0] = (header->service_identifier | LIN_SID_RESPONSE);
|
||||||
|
(*return_len) = 1;
|
||||||
|
|
||||||
|
if (header->message_type == STATUS_FRAME_HEATER && header->message_length == sizeof(StatusFrameHeater)) {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameHeater");
|
||||||
|
// Example:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|tRoom|mo| |elecA|tWate|elecB|mi|mi|cWate|cRoom|st|err | |
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.14.33.00.12.00.00.00.00.00.00.00.00.00.00.01.01.CC.0B.6C.0B.00.00.00.00
|
||||||
|
this->status_heater_ = statusFrame->inner.heater;
|
||||||
|
this->status_heater_valid_ = true;
|
||||||
|
this->status_heater_updated_ = true;
|
||||||
|
|
||||||
|
this->update_status_heater_stale_ = false;
|
||||||
|
return response;
|
||||||
|
} else if (header->message_type == STATUS_FRAME_TIMER && header->message_length == sizeof(StatusFrameTimer)) {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameTimer");
|
||||||
|
// EXAMPLE:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.18.3D.00.1D.18.0B.01.00.00.00.00.00.00.00.01.01.00.00.00.00.00.00.00.01.00.08.00.09
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.18.3D.00.13.18.0B.0B.00.00.00.00.00.00.00.01.01.00.00.00.00.00.00.00.01.00.08.00.09
|
||||||
|
this->status_timer_ = statusFrame->inner.timer;
|
||||||
|
this->status_timer_valid_ = true;
|
||||||
|
this->status_timer_updated_ = true;
|
||||||
|
|
||||||
|
this->update_status_timer_stale_ = false;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "StatusFrameTimer target_temp_room: %f target_temp_water: %f %u:%u -> %u:%u %s",
|
||||||
|
temp_code_to_decimal(this->status_timer_.timer_target_temp_room),
|
||||||
|
temp_code_to_decimal(this->status_timer_.timer_target_temp_water), this->status_timer_.timer_start_hours,
|
||||||
|
this->status_timer_.timer_start_minutes, this->status_timer_.timer_stop_hours,
|
||||||
|
this->status_timer_.timer_stop_minutes, ((u_int8_t) this->status_timer_.timer_active ? " ON" : " OFF"));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else if (header->message_type == STATUS_FRAME_RESPONSE_ACK &&
|
||||||
|
header->message_length == sizeof(StatusFrameResponseAck)) {
|
||||||
|
// Example:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.02.0D.01.98.02.00
|
||||||
|
auto data = statusFrame->inner.responseAck;
|
||||||
|
|
||||||
|
if (data.error_code != ResponseAckResult::RESPONSE_ACK_RESULT_OKAY) {
|
||||||
|
ESP_LOGW(TAG, "StatusFrameResponseAck");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameResponseAck");
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "StatusFrameResponseAck %02x %s %02x", statusFrame->inner.genericHeader.command_counter,
|
||||||
|
data.error_code == ResponseAckResult::RESPONSE_ACK_RESULT_OKAY ? " OKAY " : " FAILED ",
|
||||||
|
(u_int8_t) data.error_code);
|
||||||
|
|
||||||
|
if (data.error_code != ResponseAckResult::RESPONSE_ACK_RESULT_OKAY) {
|
||||||
|
// I tried to update something and it failed. Read current state again to validate and hold any updates for now.
|
||||||
|
this->lin_reset_device();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else if (header->message_type == STATUS_FRAME_CLOCK && header->message_length == sizeof(StatusFrameClock)) {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameClock");
|
||||||
|
// Example:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.5B.0D.20.00.01.01.00.00.01.00.00
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.71.16.00.00.01.01.00.00.02.00.00
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.2B.16.1F.28.01.01.00.00.01.00.00
|
||||||
|
this->status_clock_ = statusFrame->inner.clock;
|
||||||
|
this->status_clock_valid_ = true;
|
||||||
|
this->status_clock_updated_ = true;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "StatusFrameClock %02d:%02d:%02d", this->status_clock_.clock_hour, this->status_clock_.clock_minute,
|
||||||
|
this->status_clock_.clock_second);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else if (header->message_type == STAUTS_FRAME_CONFIG && header->message_length == sizeof(StatusFrameConfig)) {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameConfig");
|
||||||
|
// Example:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.0F.06.01.B4.0A.AA.0A.00.00.00.00
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.41.06.01.B4.0A.78.0A.00.00.00.00
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.0F.06.01.B4.0A.AA.0A.00.00.00.00
|
||||||
|
this->status_config_ = statusFrame->inner.config;
|
||||||
|
this->status_config_valid_ = true;
|
||||||
|
this->status_config_updated_ = true;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "StatusFrameConfig Offset: %d", offset_code_to_decimal(this->status_config_.temp_offset));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else if (header->message_type == STATUS_FRAME_DEVICES && header->message_length == sizeof(StatusFrameDevice)) {
|
||||||
|
ESP_LOGI(TAG, "StatusFrameDevice");
|
||||||
|
// This message is special. I recieve one response per registered (at CP plus) device.
|
||||||
|
// Example:
|
||||||
|
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0C.0B.00.79.02.00.01.00.50.00.00.04.03.02.AD.10 - C4.03.02 0050.00
|
||||||
|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0C.0B.00.27.02.01.01.00.40.03.22.02.00.01.00.00 - H2.00.01 0340.22
|
||||||
|
auto device = statusFrame->inner.device;
|
||||||
|
|
||||||
|
this->init_recieved_ = esp_timer_get_time();
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "StatusFrameDevice %d/%d - %d.%02d.%02d %04x.%02x", device.device_id + 1, device.device_count,
|
||||||
|
device.software_revision[0], device.software_revision[1], device.software_revision[2],
|
||||||
|
device.hardware_revision_major, device.hardware_revision_minor);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Unkown message type %02x", header->message_type);
|
||||||
|
}
|
||||||
|
(*return_len) = 1;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::has_update_to_submit_() {
|
||||||
|
if (this->init_requested_ == 0) {
|
||||||
|
this->init_requested_ = esp_timer_get_time();
|
||||||
|
ESP_LOGV(TAG, "Requesting initial data.");
|
||||||
|
return true;
|
||||||
|
} else if (this->init_recieved_ == 0) {
|
||||||
|
auto init_wait_time = esp_timer_get_time() - this->init_requested_;
|
||||||
|
// it has been 5 seconds and i am still awaiting the init data.
|
||||||
|
if (init_wait_time > 1000 * 1000 * 5) {
|
||||||
|
ESP_LOGV(TAG, "Requesting initial data again.");
|
||||||
|
this->init_requested_ = esp_timer_get_time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (this->update_status_heater_unsubmitted_ || this->update_status_timer_unsubmitted_ ||
|
||||||
|
this->update_status_clock_unsubmitted_) {
|
||||||
|
if (this->update_time_ == 0) {
|
||||||
|
ESP_LOGV(TAG, "Notify CP Plus I got updates.");
|
||||||
|
this->update_time_ = esp_timer_get_time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto update_wait_time = esp_timer_get_time() - this->update_time_;
|
||||||
|
if (update_wait_time > 1000 * 1000 * 5) {
|
||||||
|
ESP_LOGV(TAG, "Notify CP Plus again I still got updates.");
|
||||||
|
this->update_time_ = esp_timer_get_time();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
461
components/truma_inetbox/TrumaiNetBoxApp.h
Normal file
461
components/truma_inetbox/TrumaiNetBoxApp.h
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "LinBusProtocol.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/components/time/real_time_clock.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
#define LIN_PID_TRUMA_INET_BOX 0x18
|
||||||
|
#define LIN_SID_RESPONSE 0x40
|
||||||
|
#define LIN_SID_READ_STATE_BUFFER 0xBA
|
||||||
|
#define LIN_SID_FIll_STATE_BUFFFER 0xBB
|
||||||
|
|
||||||
|
// Response to init are the following frames:
|
||||||
|
// - 2 * STATUS_FRAME_DEVICES
|
||||||
|
// - STATUS_FRAME_HEATER
|
||||||
|
// - STATUS_FRAME_TIMER
|
||||||
|
// - STAUTS_FRAME_CONFIG
|
||||||
|
// - STATUS_FRAME_CLOCK
|
||||||
|
#define STATUS_FRAME_RESPONSE_INIT_REQUEST 0x0A
|
||||||
|
#define STATUS_FRAME_DEVICES 0x0B
|
||||||
|
#define STATUS_FRAME_RESPONSE_ACK 0x0D
|
||||||
|
#define STATUS_FRAME_CLOCK 0x15
|
||||||
|
#define STATUS_FRAME_CLOCK_RESPONSE (STATUS_FRAME_CLOCK - 1)
|
||||||
|
#define STAUTS_FRAME_CONFIG 0x17
|
||||||
|
#define STAUTS_FRAME_CONFIG_RESPONSE (STAUTS_FRAME_CONFIG - 1)
|
||||||
|
// Why can I send 0x33 as response?
|
||||||
|
#define STATUS_FRAME_HEATER 0x33
|
||||||
|
#define STATUS_FRAME_HEATER_RESPONSE (STATUS_FRAME_HEATER - 1)
|
||||||
|
// Error response
|
||||||
|
#define STATUS_FRAME_UNKOWN_34 0x34
|
||||||
|
// Error response
|
||||||
|
#define STATUS_FRAME_UNKOWN_36 0x36
|
||||||
|
#define STATUS_FRAME_TIMER 0x3D
|
||||||
|
#define STATUS_FRAME_TIMER_RESPONSE (STATUS_FRAME_TIMER - 1)
|
||||||
|
// Error response
|
||||||
|
#define STATUS_FRAME_UNKOWN_3E 0x3E
|
||||||
|
|
||||||
|
enum class HeatingMode : u_int8_t {
|
||||||
|
HEATING_MODE_OFF = 0x0,
|
||||||
|
HEATING_MODE_ECO = 0x1,
|
||||||
|
HEATING_MODE_HIGH = 0xA,
|
||||||
|
HEATING_MODE_BOOST = 0xB,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ElectricPowerLevel : u_int16_t {
|
||||||
|
ELECTRIC_POWER_LEVEL_0 = 0,
|
||||||
|
ELECTRIC_POWER_LEVEL_900 = 900,
|
||||||
|
ELECTRIC_POWER_LEVEL_1800 = 1800,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TargetTemp : u_int16_t {
|
||||||
|
TARGET_TEMP_OFF = 0x0,
|
||||||
|
|
||||||
|
// 40C
|
||||||
|
TARGET_TEMP_WATER_ECO = (40 + 273) * 10,
|
||||||
|
// 60C
|
||||||
|
TARGET_TEMP_WATER_HIGH = (60 + 273) * 10,
|
||||||
|
// 200C
|
||||||
|
TARGET_TEMP_WATER_BOOST = (200 + 273) * 10,
|
||||||
|
|
||||||
|
TARGET_TEMP_ROOM_MIN = (5 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_05C = (5 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_06C = (6 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_07C = (7 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_08C = (8 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_09C = (9 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_10C = (10 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_11C = (11 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_12C = (12 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_13C = (13 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_14C = (14 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_15C = (15 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_16C = (16 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_17C = (17 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_18C = (18 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_19C = (19 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_20C = (20 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_21C = (21 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_22C = (22 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_23C = (23 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_24C = (24 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_25C = (25 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_26C = (26 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_27C = (27 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_28C = (28 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_29C = (29 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_30C = (30 + 273) * 10,
|
||||||
|
TARGET_TEMP_ROOM_MAX = (30 + 273) * 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class EnergyMix : u_int8_t {
|
||||||
|
ENERGY_MIX_NONE = 0b00,
|
||||||
|
ENERGY_MIX_GAS = 0b01,
|
||||||
|
ENERGY_MIX_ELECTRICITY = 0b10,
|
||||||
|
ENERGY_MIX_MIX = 0b11,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OperatingStatus : u_int8_t {
|
||||||
|
OPERATING_STATUS_UNSET = 0x0,
|
||||||
|
OPERATING_STATUS_OFF = 0x0,
|
||||||
|
OPERATING_STATUS_WARNING = 0x1,
|
||||||
|
OPERATING_STATUS_START_OR_COOL_DOWN = 0x4,
|
||||||
|
// ON - Heater off
|
||||||
|
OPERATING_STATUS_ON_5 = 0x5,
|
||||||
|
OPERATING_STATUS_ON_6 = 0x6,
|
||||||
|
OPERATING_STATUS_ON_7 = 0x7,
|
||||||
|
OPERATING_STATUS_ON_8 = 0x8,
|
||||||
|
OPERATING_STATUS_ON_9 = 0x9,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OperatingUnits : u_int8_t {
|
||||||
|
OPERATING_UNITS_CELSIUS = 0x0,
|
||||||
|
OPERATING_UNITS_FAHRENHEIT = 0x1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Language : u_int8_t {
|
||||||
|
LANGUAGE_GERMAN = 0x0,
|
||||||
|
LANGUAGE_ENGLISH = 0x1,
|
||||||
|
LANGUAGE_FRENCH = 0x2,
|
||||||
|
LANGUAGE_ITALY = 0x3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ResponseAckResult : u_int8_t {
|
||||||
|
RESPONSE_ACK_RESULT_OKAY = 0x0,
|
||||||
|
RESPONSE_ACK_RESULT_ERROR_INVALID_MSG = 0x2,
|
||||||
|
// The response status frame `message_type` is unkown.
|
||||||
|
RESPONSE_ACK_RESULT_ERROR_INVALID_ID = 0x3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TempOffset : u_int8_t {
|
||||||
|
TEMP_OFFSET_0_0C = (u_int8_t) ((-0.0f + 17) * 10),
|
||||||
|
TEMP_OFFSET_0_5C = (u_int8_t) ((-0.5f + 17) * 10),
|
||||||
|
TEMP_OFFSET_1_0C = (u_int8_t) ((-1.0f + 17) * 10),
|
||||||
|
TEMP_OFFSET_1_5C = (u_int8_t) ((-1.5f + 17) * 10),
|
||||||
|
TEMP_OFFSET_2_0C = (u_int8_t) ((-2.0f + 17) * 10),
|
||||||
|
TEMP_OFFSET_2_5C = (u_int8_t) ((-2.5f + 17) * 10),
|
||||||
|
TEMP_OFFSET_3_0C = (u_int8_t) ((-3.0f + 17) * 10),
|
||||||
|
TEMP_OFFSET_3_5C = (u_int8_t) ((-3.5f + 17) * 10),
|
||||||
|
TEMP_OFFSET_4_0C = (u_int8_t) ((-4.0f + 17) * 10),
|
||||||
|
TEMP_OFFSET_4_5C = (u_int8_t) ((-4.5f + 17) * 10),
|
||||||
|
TEMP_OFFSET_5_0C = (u_int8_t) ((-5.0f + 17) * 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ClockMode : u_int8_t {
|
||||||
|
CLOCK_MODE_24H = 0x0,
|
||||||
|
CLOCK_MODE_12H = 0x1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TimerActive : u_int8_t {
|
||||||
|
TIMER_ACTIVE_ON = 0x1,
|
||||||
|
TIMER_ACTIVE_OFF = 0x0,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ClockSource : u_int8_t {
|
||||||
|
// Set by user
|
||||||
|
CLOCK_SOURCE_MANUAL = 0x1,
|
||||||
|
// Set by message
|
||||||
|
CLOCK_SOURCE_PROG = 0x2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StatusFrameHeader { // NOLINT(altera-struct-pack-align)
|
||||||
|
// sid
|
||||||
|
u_int8_t service_identifier;
|
||||||
|
u_int8_t header[10];
|
||||||
|
u_int8_t header_2;
|
||||||
|
u_int8_t header_3;
|
||||||
|
// after checksum
|
||||||
|
u_int8_t message_length;
|
||||||
|
u_int8_t message_type;
|
||||||
|
u_int8_t command_counter;
|
||||||
|
u_int8_t checksum;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 20 (0x14)
|
||||||
|
struct StatusFrameHeater { // NOLINT(altera-struct-pack-align)
|
||||||
|
TargetTemp target_temp_room;
|
||||||
|
// Room
|
||||||
|
HeatingMode heating_mode;
|
||||||
|
u_int8_t heater_unkown_1;
|
||||||
|
ElectricPowerLevel el_power_level_a;
|
||||||
|
TargetTemp target_temp_water;
|
||||||
|
ElectricPowerLevel el_power_level_b;
|
||||||
|
EnergyMix energy_mix_a;
|
||||||
|
// Ignored by response
|
||||||
|
EnergyMix energy_mix_b;
|
||||||
|
u_int16_t current_temp_water;
|
||||||
|
u_int16_t current_temp_room;
|
||||||
|
OperatingStatus operating_status;
|
||||||
|
u_int16_t error_code;
|
||||||
|
u_int8_t heater_unkown_2;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 12 (0x0C)
|
||||||
|
struct StatusFrameHeaterResponse { // NOLINT(altera-struct-pack-align)
|
||||||
|
TargetTemp target_temp_room;
|
||||||
|
// Room
|
||||||
|
HeatingMode heating_mode;
|
||||||
|
u_int8_t recv_status_u3;
|
||||||
|
ElectricPowerLevel el_power_level_a;
|
||||||
|
TargetTemp target_temp_water;
|
||||||
|
ElectricPowerLevel el_power_level_b;
|
||||||
|
EnergyMix energy_mix_a;
|
||||||
|
// Ignored?
|
||||||
|
EnergyMix energy_mix_b;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 24 (0x18)
|
||||||
|
struct StatusFrameTimer { // NOLINT(altera-struct-pack-align)
|
||||||
|
TargetTemp timer_target_temp_room;
|
||||||
|
HeatingMode timer_heating_mode;
|
||||||
|
u_int8_t timer_unkown_1;
|
||||||
|
ElectricPowerLevel timer_el_power_level_a;
|
||||||
|
TargetTemp timer_target_temp_water;
|
||||||
|
ElectricPowerLevel timer_el_power_level_b;
|
||||||
|
EnergyMix timer_energy_mix_a;
|
||||||
|
EnergyMix timer_energy_mix_b;
|
||||||
|
// used by timer response message
|
||||||
|
u_int8_t unused[5];
|
||||||
|
u_int8_t timer_unknown_3;
|
||||||
|
u_int8_t timer_unknown_4;
|
||||||
|
TimerActive timer_active;
|
||||||
|
u_int8_t timer_start_minutes;
|
||||||
|
u_int8_t timer_start_hours;
|
||||||
|
u_int8_t timer_stop_minutes;
|
||||||
|
u_int8_t timer_stop_hours;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 13 (0x0D)
|
||||||
|
struct StatusFrameTimerResponse { // NOLINT(altera-struct-pack-align)
|
||||||
|
TargetTemp timer_target_temp_room;
|
||||||
|
HeatingMode timer_heating_mode;
|
||||||
|
u_int8_t timer_unkown_1;
|
||||||
|
ElectricPowerLevel timer_el_power_level_a;
|
||||||
|
TargetTemp timer_target_temp_water;
|
||||||
|
ElectricPowerLevel timer_el_power_level_b;
|
||||||
|
EnergyMix timer_energy_mix_a;
|
||||||
|
EnergyMix timer_energy_mix_b;
|
||||||
|
// set by response message to active timer
|
||||||
|
TimerActive timer_resp_active;
|
||||||
|
// set by response message to active timer
|
||||||
|
u_int8_t timer_resp_start_minutes;
|
||||||
|
// set by response message to active timer
|
||||||
|
u_int8_t timer_resp_start_hours;
|
||||||
|
// set by response message to active timer
|
||||||
|
u_int8_t timer_resp_stop_minutes;
|
||||||
|
// set by response message to active timer
|
||||||
|
u_int8_t timer_resp_stop_hours;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 2 (0x02)
|
||||||
|
struct StatusFrameResponseAck { // NOLINT(altera-struct-pack-align)
|
||||||
|
ResponseAckResult error_code;
|
||||||
|
u_int8_t unkown;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 10 (0x0A)
|
||||||
|
struct StatusFrameClock { // NOLINT(altera-struct-pack-align)
|
||||||
|
u_int8_t clock_hour;
|
||||||
|
u_int8_t clock_minute;
|
||||||
|
u_int8_t clock_second;
|
||||||
|
// MUST be 0x1, 0x2, 0x3..? (lower than 0x9)
|
||||||
|
u_int8_t display_1;
|
||||||
|
// MUST be 0x1
|
||||||
|
u_int8_t display_2;
|
||||||
|
u_int8_t display_3;
|
||||||
|
ClockMode clock_mode;
|
||||||
|
ClockSource clock_source;
|
||||||
|
u_int8_t display_4;
|
||||||
|
u_int8_t display_5;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 10 (0x0A)
|
||||||
|
struct StatusFrameConfig { // NOLINT(altera-struct-pack-align)
|
||||||
|
// 0x01 .. 0x0A
|
||||||
|
u_int8_t display_brightness;
|
||||||
|
Language language;
|
||||||
|
u_int8_t unkown_2; // 0xB4
|
||||||
|
u_int8_t unkown_3; // 0x0A
|
||||||
|
TempOffset temp_offset;
|
||||||
|
u_int8_t unkown_5; // 0x0A
|
||||||
|
OperatingUnits temp_units;
|
||||||
|
u_int8_t unkown_6;
|
||||||
|
u_int8_t unkown_7;
|
||||||
|
u_int8_t unkown_8;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// Length 12 (0x0C)
|
||||||
|
struct StatusFrameDevice { // NOLINT(altera-struct-pack-align)
|
||||||
|
u_int8_t device_count;
|
||||||
|
u_int8_t device_id;
|
||||||
|
// 0x01 - Maybe active or found
|
||||||
|
u_int8_t unkown_0;
|
||||||
|
// 0x00
|
||||||
|
u_int8_t unkown_1;
|
||||||
|
u_int16_t hardware_revision_major;
|
||||||
|
u_int8_t hardware_revision_minor;
|
||||||
|
// `software_revision[0].software_revision[1].software_revision[2]`
|
||||||
|
u_int8_t software_revision[3];
|
||||||
|
// 0xAD on CPplus
|
||||||
|
// 0x00 on Combi4
|
||||||
|
u_int8_t unkown_2;
|
||||||
|
// 0x10 on CPplus
|
||||||
|
// 0x00 on Combi4
|
||||||
|
u_int8_t unkown_3;
|
||||||
|
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
union StatusFrame { // NOLINT(altera-struct-pack-align)
|
||||||
|
u_int8_t raw[41];
|
||||||
|
struct inner { // NOLINT(altera-struct-pack-align)
|
||||||
|
StatusFrameHeader genericHeader;
|
||||||
|
union { // NOLINT(altera-struct-pack-align)
|
||||||
|
StatusFrameHeater heater;
|
||||||
|
StatusFrameHeaterResponse heaterResponse;
|
||||||
|
StatusFrameTimer timer;
|
||||||
|
StatusFrameTimerResponse timerResponse;
|
||||||
|
StatusFrameResponseAck responseAck;
|
||||||
|
StatusFrameClock clock;
|
||||||
|
StatusFrameConfig config;
|
||||||
|
StatusFrameDevice device;
|
||||||
|
} __attribute__((packed));
|
||||||
|
} inner;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
struct StatusFrameListener {
|
||||||
|
std::function<void(const StatusFrameHeater *)> on_heater_change;
|
||||||
|
std::function<void(const StatusFrameTimer *)> on_timer_change;
|
||||||
|
std::function<void(const StatusFrameClock *)> on_clock_change;
|
||||||
|
std::function<void(const StatusFrameConfig *)> on_config_change;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrumaiNetBoxApp : public LinBusProtocol {
|
||||||
|
public:
|
||||||
|
TrumaiNetBoxApp(u_int8_t expected_listener_count);
|
||||||
|
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
const std::array<u_int8_t, 4> lin_identifier() override;
|
||||||
|
void lin_reset_device() override;
|
||||||
|
|
||||||
|
void set_time(time::RealTimeClock *time) { time_ = time; }
|
||||||
|
time::RealTimeClock *get_time() const { return time_; }
|
||||||
|
|
||||||
|
bool get_status_heater_valid() { return this->status_heater_valid_; }
|
||||||
|
const StatusFrameHeater *get_status_heater() { return &this->status_heater_; }
|
||||||
|
void register_listener(const std::function<void(const StatusFrameHeater *)> &func);
|
||||||
|
|
||||||
|
bool get_status_timer_valid() { return this->status_timer_valid_; }
|
||||||
|
const StatusFrameTimer *get_status_timer() { return &this->status_timer_; }
|
||||||
|
void register_listener(const std::function<void(const StatusFrameTimer *)> &func);
|
||||||
|
|
||||||
|
bool get_status_clock_valid() { return this->status_clock_valid_; }
|
||||||
|
const StatusFrameClock *get_status_clock() { return &this->status_clock_; }
|
||||||
|
void register_listener(const std::function<void(const StatusFrameClock *)> &func);
|
||||||
|
|
||||||
|
bool get_status_config_valid() { return this->status_config_valid_; }
|
||||||
|
const StatusFrameConfig *get_status_config() { return &this->status_config_; }
|
||||||
|
void register_listener(const std::function<void(const StatusFrameConfig *)> &func);
|
||||||
|
|
||||||
|
bool truma_heater_can_update() { return this->status_heater_valid_; }
|
||||||
|
StatusFrameHeaterResponse *update_heater_prepare();
|
||||||
|
void update_heater_submit() { this->update_status_heater_unsubmitted_ = true; }
|
||||||
|
|
||||||
|
bool truma_timer_can_update() { return this->status_timer_valid_; }
|
||||||
|
StatusFrameTimerResponse *update_timer_prepare();
|
||||||
|
void update_timer_submit() { this->update_status_timer_unsubmitted_ = true; }
|
||||||
|
|
||||||
|
bool truma_clock_can_update() { return this->status_clock_valid_; }
|
||||||
|
void update_clock_submit() { this->update_status_clock_unsubmitted_ = true; }
|
||||||
|
|
||||||
|
int64_t get_last_cp_plus_request() { return this->device_registered_; }
|
||||||
|
|
||||||
|
// Automation
|
||||||
|
void add_on_heater_message_callback(std::function<void(const StatusFrameHeater *)> callback) {
|
||||||
|
this->state_heater_callback_.add(std::move(callback));
|
||||||
|
}
|
||||||
|
bool action_heater_room(u_int8_t temperature, HeatingMode mode = HeatingMode::HEATING_MODE_OFF);
|
||||||
|
bool action_heater_water(u_int8_t temperature);
|
||||||
|
bool action_heater_water(TargetTemp temperature);
|
||||||
|
bool action_heater_electric_power_level(u_int16_t value);
|
||||||
|
bool action_heater_energy_mix(EnergyMix energy_mix,
|
||||||
|
ElectricPowerLevel el_power_level = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0);
|
||||||
|
bool action_timer_disable();
|
||||||
|
bool action_timer_activate(u_int16_t start, u_int16_t stop, u_int8_t room_temperature,
|
||||||
|
HeatingMode mode = HeatingMode::HEATING_MODE_OFF, u_int8_t water_temperature = 0,
|
||||||
|
EnergyMix energy_mix = EnergyMix::ENERGY_MIX_NONE,
|
||||||
|
ElectricPowerLevel el_power_level = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0);
|
||||||
|
bool action_read_time();
|
||||||
|
bool action_write_time();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
time::RealTimeClock *time_;
|
||||||
|
|
||||||
|
// Truma CP Plus needs init (reset). This device is not registered.
|
||||||
|
int64_t device_registered_ = 0;
|
||||||
|
int64_t init_requested_ = 0;
|
||||||
|
int64_t init_recieved_ = 0;
|
||||||
|
u_int8_t message_counter = 1;
|
||||||
|
|
||||||
|
std::vector<StatusFrameListener> listeners_heater_;
|
||||||
|
CallbackManager<void(const StatusFrameHeater *)> state_heater_callback_{};
|
||||||
|
|
||||||
|
bool status_heater_valid_ = false;
|
||||||
|
// Value has changed notify listeners.
|
||||||
|
bool status_heater_updated_ = false;
|
||||||
|
StatusFrameHeater status_heater_;
|
||||||
|
|
||||||
|
bool status_timer_valid_ = false;
|
||||||
|
// Value has changed notify listeners.
|
||||||
|
bool status_timer_updated_ = false;
|
||||||
|
StatusFrameTimer status_timer_;
|
||||||
|
|
||||||
|
bool status_clock_valid_ = false;
|
||||||
|
// Value has changed notify listeners.
|
||||||
|
bool status_clock_updated_ = false;
|
||||||
|
StatusFrameClock status_clock_;
|
||||||
|
|
||||||
|
bool status_config_valid_ = false;
|
||||||
|
// Value has changed notify listeners.
|
||||||
|
bool status_config_updated_ = false;
|
||||||
|
StatusFrameConfig status_config_;
|
||||||
|
|
||||||
|
// last time CP plus was informed I got an update msg.
|
||||||
|
int64_t update_time_ = 0;
|
||||||
|
// Prepared means `update_status_heater_` was copied from `status_heater_`.
|
||||||
|
bool update_status_heater_prepared_ = false;
|
||||||
|
// Prepared means an update is already awating fetch from CP plus.
|
||||||
|
bool update_status_heater_unsubmitted_ = false;
|
||||||
|
// I have submitted my update request to CP plus, but I have not recieved an update with new heater values from CP
|
||||||
|
// plus.
|
||||||
|
bool update_status_heater_stale_ = false;
|
||||||
|
StatusFrameHeaterResponse update_status_heater_;
|
||||||
|
|
||||||
|
// Prepared means `update_status_timer_` was copied from `status_timer_`.
|
||||||
|
bool update_status_timer_prepared_ = false;
|
||||||
|
// Prepared means an update is already awating fetch from CP plus.
|
||||||
|
bool update_status_timer_unsubmitted_ = false;
|
||||||
|
// I have submitted my update request to CP plus, but I have not recieved an update with new timer values from CP
|
||||||
|
// plus.
|
||||||
|
bool update_status_timer_stale_ = false;
|
||||||
|
StatusFrameTimerResponse update_status_timer_;
|
||||||
|
|
||||||
|
// The behaviour of `update_status_clock_unsubmitted_` is special.
|
||||||
|
// Just an update is marked. The actual package is prepared when CP Plus asks for the data in the
|
||||||
|
// `lin_multiframe_recieved` method.
|
||||||
|
bool update_status_clock_unsubmitted_ = false;
|
||||||
|
|
||||||
|
bool answer_lin_order_(const u_int8_t pid) override;
|
||||||
|
|
||||||
|
bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) override;
|
||||||
|
const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
|
||||||
|
u_int8_t *return_len) override;
|
||||||
|
|
||||||
|
bool has_update_to_submit_();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
250
components/truma_inetbox/TrumaiNetBoxApp_automation.cpp
Normal file
250
components/truma_inetbox/TrumaiNetBoxApp_automation.cpp
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
#include "TrumaStatusFrame.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.TrumaiNetBoxApp";
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_heater_room(u_int8_t temperature, HeatingMode mode) {
|
||||||
|
if (!this->truma_heater_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto heater = this->update_heater_prepare();
|
||||||
|
|
||||||
|
heater->target_temp_room = decimal_to_room_temp(temperature);
|
||||||
|
|
||||||
|
// Ensure `heating_mode` and `energy_mix_a` is set.
|
||||||
|
if (heater->target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
|
||||||
|
heater->heating_mode = HeatingMode::HEATING_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// If parameter `mode` contains a valid Heating mode use it or else use `ECO`.
|
||||||
|
if (mode == HeatingMode::HEATING_MODE_ECO || mode == HeatingMode::HEATING_MODE_HIGH ||
|
||||||
|
mode == HeatingMode::HEATING_MODE_BOOST) {
|
||||||
|
heater->heating_mode = mode;
|
||||||
|
} else if (heater->heating_mode == HeatingMode::HEATING_MODE_OFF) {
|
||||||
|
heater->heating_mode = HeatingMode::HEATING_MODE_ECO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_heater_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_heater_water(u_int8_t temperature) {
|
||||||
|
if (!this->truma_heater_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto heater = this->update_heater_prepare();
|
||||||
|
|
||||||
|
heater->target_temp_water = deciaml_to_water_temp(temperature);
|
||||||
|
|
||||||
|
// Ensure `energy_mix_a` is set.
|
||||||
|
if (heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF && heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_heater_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_heater_water(TargetTemp temperature) {
|
||||||
|
if (!this->truma_heater_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto heater = this->update_heater_prepare();
|
||||||
|
|
||||||
|
// If parameter `temperature` contains a valid mode use it or else use `OFF`.
|
||||||
|
if (temperature == TargetTemp::TARGET_TEMP_WATER_ECO || temperature == TargetTemp::TARGET_TEMP_WATER_HIGH ||
|
||||||
|
temperature == TargetTemp::TARGET_TEMP_WATER_BOOST) {
|
||||||
|
heater->target_temp_water = temperature;
|
||||||
|
} else {
|
||||||
|
heater->target_temp_water = TargetTemp::TARGET_TEMP_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure `energy_mix_a` is set.
|
||||||
|
if (heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF && heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_heater_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_heater_electric_power_level(u_int16_t value) {
|
||||||
|
if (!this->truma_heater_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto heater = this->update_heater_prepare();
|
||||||
|
|
||||||
|
heater->el_power_level_a = decimal_to_el_power_level(value);
|
||||||
|
if (heater->el_power_level_a != ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
|
||||||
|
if (heater->energy_mix_a != EnergyMix::ENERGY_MIX_MIX &&
|
||||||
|
heater->energy_mix_a != EnergyMix::ENERGY_MIX_ELECTRICITY) {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_MIX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_heater_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_heater_energy_mix(EnergyMix energy_mix, ElectricPowerLevel el_power_level) {
|
||||||
|
if (!this->truma_heater_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto heater = this->update_heater_prepare();
|
||||||
|
|
||||||
|
// If parameter `el_power_level` contains a valid mode use it.
|
||||||
|
if (el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0 ||
|
||||||
|
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900 ||
|
||||||
|
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800) {
|
||||||
|
heater->el_power_level_a = el_power_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (energy_mix == EnergyMix::ENERGY_MIX_GAS) {
|
||||||
|
heater->energy_mix_a = energy_mix;
|
||||||
|
heater->el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
|
||||||
|
} else if (energy_mix == EnergyMix::ENERGY_MIX_MIX || energy_mix == EnergyMix::ENERGY_MIX_ELECTRICITY) {
|
||||||
|
heater->energy_mix_a = energy_mix;
|
||||||
|
// Electric energy is requested by user without a power level. Set it to minimum.
|
||||||
|
if (heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
|
||||||
|
heater->el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This last check is reached if invalid `energy_mix` parameter was submitted.
|
||||||
|
if (heater->el_power_level_a != ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
|
||||||
|
if (heater->energy_mix_a != EnergyMix::ENERGY_MIX_MIX &&
|
||||||
|
heater->energy_mix_a != EnergyMix::ENERGY_MIX_ELECTRICITY) {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_MIX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_heater_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_timer_disable() {
|
||||||
|
if (!this->truma_timer_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto timer = this->update_timer_prepare();
|
||||||
|
|
||||||
|
timer->timer_resp_active = TimerActive::TIMER_ACTIVE_OFF;
|
||||||
|
|
||||||
|
this->update_timer_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_timer_activate(u_int16_t start, u_int16_t stop, u_int8_t room_temperature,
|
||||||
|
HeatingMode mode, u_int8_t water_temperature, EnergyMix energy_mix,
|
||||||
|
ElectricPowerLevel el_power_level) {
|
||||||
|
if (!this->truma_timer_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (start > 1440 || stop > 1440) {
|
||||||
|
ESP_LOGW(TAG, "Invalid values start/stop submitted.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto timer = this->update_timer_prepare();
|
||||||
|
|
||||||
|
timer->timer_resp_active = TimerActive::TIMER_ACTIVE_ON;
|
||||||
|
timer->timer_resp_start_hours = start / 60;
|
||||||
|
timer->timer_resp_start_minutes = start % 60;
|
||||||
|
timer->timer_resp_stop_hours = stop / 60;
|
||||||
|
timer->timer_resp_stop_minutes = stop % 60;
|
||||||
|
timer->timer_target_temp_room = decimal_to_room_temp(room_temperature);
|
||||||
|
|
||||||
|
// Ensure `timer_heating_mode` and `timer_energy_mix_a` is set.
|
||||||
|
if (timer->timer_target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
|
||||||
|
timer->timer_heating_mode = HeatingMode::HEATING_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// If parameter `mode` contains a valid Heating mode use it or else use `ECO`.
|
||||||
|
if (mode == HeatingMode::HEATING_MODE_ECO || mode == HeatingMode::HEATING_MODE_HIGH ||
|
||||||
|
mode == HeatingMode::HEATING_MODE_BOOST) {
|
||||||
|
timer->timer_heating_mode = mode;
|
||||||
|
} else if (timer->timer_heating_mode == HeatingMode::HEATING_MODE_OFF) {
|
||||||
|
timer->timer_heating_mode = HeatingMode::HEATING_MODE_ECO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer->timer_target_temp_water = deciaml_to_water_temp(water_temperature);
|
||||||
|
|
||||||
|
// If parameter `el_power_level` contains a valid mode use it.
|
||||||
|
if (el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0 ||
|
||||||
|
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900 ||
|
||||||
|
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800) {
|
||||||
|
timer->timer_el_power_level_a = el_power_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure `timer_energy_mix_a` is set
|
||||||
|
if (timer->timer_energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
|
||||||
|
timer->timer_energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User has supplied a `energy_mix`
|
||||||
|
if (energy_mix == EnergyMix::ENERGY_MIX_GAS) {
|
||||||
|
timer->timer_energy_mix_a = energy_mix;
|
||||||
|
timer->timer_el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
|
||||||
|
} else if (energy_mix == EnergyMix::ENERGY_MIX_MIX || energy_mix == EnergyMix::ENERGY_MIX_ELECTRICITY) {
|
||||||
|
timer->timer_energy_mix_a = energy_mix;
|
||||||
|
// Electric energy is requested by user without a power level. Set it to minimum.
|
||||||
|
if (timer->timer_el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
|
||||||
|
timer->timer_el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_timer_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_read_time() {
|
||||||
|
// int ret = settimeofday(&timev, &tz);
|
||||||
|
// if (ret == EINVAL) {
|
||||||
|
// // Some ESP8266 frameworks abort when timezone parameter is not NULL
|
||||||
|
// // while ESP32 expects it not to be NULL
|
||||||
|
// ret = settimeofday(&timev, nullptr);
|
||||||
|
// }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrumaiNetBoxApp::action_write_time() {
|
||||||
|
if (!this->truma_clock_can_update()) {
|
||||||
|
ESP_LOGW(TAG, "Cannot update Truma.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto now = this->time_->now();
|
||||||
|
if (!now.is_valid()) {
|
||||||
|
ESP_LOGW(TAG, "Invalid system time, not syncing to CP Plus.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The behaviour of this method is special.
|
||||||
|
// Just an update is marked. The actual package is prepared when CP Plus asks for the data in the
|
||||||
|
// `lin_multiframe_recieved` method.
|
||||||
|
|
||||||
|
this->update_clock_submit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
366
components/truma_inetbox/__init__.py
Normal file
366
components/truma_inetbox/__init__.py
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome import pins, automation
|
||||||
|
from esphome.components import sensor, uart, time
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_CS_PIN,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
CONF_ON_MESSAGE,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
|
CONF_STOP,
|
||||||
|
CONF_TIME_ID,
|
||||||
|
)
|
||||||
|
from .entity_helpers import count_id_usage
|
||||||
|
|
||||||
|
DEPENDENCIES = ["uart"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
CONF_TRUMA_INETBOX_ID = "truma_inetbox_id"
|
||||||
|
CONF_LIN_CHECKSUM = "lin_checksum"
|
||||||
|
CONF_FAULT_PIN = "fault_pin"
|
||||||
|
CONF_OBSERVER_MODE = "observer_mode"
|
||||||
|
CONF_NUMBER_OF_CHILDREN = "number_of_children"
|
||||||
|
CONF_ON_HEATER_MESSAGE = "on_heater_message"
|
||||||
|
|
||||||
|
truma_inetbox_ns = cg.esphome_ns.namespace("truma_inetbox")
|
||||||
|
StatusFrameHeater = truma_inetbox_ns.struct("StatusFrameHeater")
|
||||||
|
StatusFrameHeaterConstPtr = StatusFrameHeater.operator("ptr").operator("const")
|
||||||
|
TrumaINetBoxApp = truma_inetbox_ns.class_(
|
||||||
|
"TrumaiNetBoxApp", cg.PollingComponent, uart.UARTDevice
|
||||||
|
)
|
||||||
|
TrumaiNetBoxAppHeaterMessageTrigger = truma_inetbox_ns.class_(
|
||||||
|
"TrumaiNetBoxAppHeaterMessageTrigger",
|
||||||
|
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
|
||||||
|
)
|
||||||
|
|
||||||
|
# `LIN_CHECKSUM` is a enum class and not a namespace but it works.
|
||||||
|
LIN_CHECKSUM_dummy_ns = truma_inetbox_ns.namespace("LIN_CHECKSUM")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_LIN_CHECKSUM = {
|
||||||
|
"VERSION_1": LIN_CHECKSUM_dummy_ns.LIN_CHECKSUM_VERSION_1,
|
||||||
|
"VERSION_2": LIN_CHECKSUM_dummy_ns.LIN_CHECKSUM_VERSION_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TrumaINetBoxApp),
|
||||||
|
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||||
|
cv.Optional(CONF_LIN_CHECKSUM, "VERSION_2"): cv.enum(CONF_SUPPORTED_LIN_CHECKSUM, upper=True),
|
||||||
|
cv.Optional(CONF_CS_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_FAULT_PIN): pins.gpio_input_pin_schema,
|
||||||
|
cv.Optional(CONF_OBSERVER_MODE): cv.boolean,
|
||||||
|
cv.Optional(CONF_ON_HEATER_MESSAGE): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TrumaiNetBoxAppHeaterMessageTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Polling is for presenting data to sensors.
|
||||||
|
# Reading and communication is done in a seperate thread/core.
|
||||||
|
.extend(cv.polling_component_schema("500ms"))
|
||||||
|
.extend(uart.UART_DEVICE_SCHEMA),
|
||||||
|
cv.only_with_arduino,
|
||||||
|
cv.only_on(["esp32"]),
|
||||||
|
)
|
||||||
|
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||||
|
uart.final_validate_device_schema(
|
||||||
|
# TODO: Validate 2 Stop bits are configured.
|
||||||
|
"truma_inetbox", baud_rate=9600, require_tx=True, require_rx=True
|
||||||
|
),
|
||||||
|
count_id_usage(CONF_NUMBER_OF_CHILDREN, [
|
||||||
|
CONF_TRUMA_INETBOX_ID, CONF_ID], TrumaINetBoxApp),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUMBER_OF_CHILDREN])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await uart.register_uart_device(var, config)
|
||||||
|
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||||
|
cg.add(var.set_time(time_))
|
||||||
|
|
||||||
|
if CONF_LIN_CHECKSUM in config:
|
||||||
|
cg.add(var.set_lin_checksum(
|
||||||
|
CONF_SUPPORTED_LIN_CHECKSUM[config[CONF_LIN_CHECKSUM]]))
|
||||||
|
|
||||||
|
if CONF_CS_PIN in config:
|
||||||
|
pin = await cg.gpio_pin_expression(config[CONF_CS_PIN])
|
||||||
|
cg.add(var.set_cs_pin(pin))
|
||||||
|
|
||||||
|
if CONF_FAULT_PIN in config:
|
||||||
|
pin = await cg.gpio_pin_expression(config[CONF_FAULT_PIN])
|
||||||
|
cg.add(var.set_fault_pin(pin))
|
||||||
|
|
||||||
|
if CONF_OBSERVER_MODE in config:
|
||||||
|
cg.add(var.set_observer_mode(config[CONF_OBSERVER_MODE]))
|
||||||
|
|
||||||
|
for conf in config.get(CONF_ON_HEATER_MESSAGE, []):
|
||||||
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
|
await automation.build_automation(
|
||||||
|
trigger, [(StatusFrameHeaterConstPtr, "message")], conf
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# AUTOMATION
|
||||||
|
|
||||||
|
CONF_ENERGY_MIX = "energy_mix"
|
||||||
|
CONF_ELECTRIC_POWER_LEVEL = "electric_power_level"
|
||||||
|
CONF_HEATING_MODE = "heating_mode"
|
||||||
|
CONF_WATT = "watt"
|
||||||
|
CONF_START = "start"
|
||||||
|
CONF_ROOM_TEMPERATURE = "room_temperature"
|
||||||
|
CONF_WATER_TEMPERATURE = "water_temperature"
|
||||||
|
|
||||||
|
HeaterRoomTempAction = truma_inetbox_ns.class_(
|
||||||
|
"HeaterRoomTempAction", automation.Action)
|
||||||
|
HeaterWaterTempAction = truma_inetbox_ns.class_(
|
||||||
|
"HeaterWaterTempAction", automation.Action)
|
||||||
|
HeaterWaterTempEnumAction = truma_inetbox_ns.class_(
|
||||||
|
"HeaterWaterTempEnumAction", automation.Action)
|
||||||
|
HeaterElecPowerLevelAction = truma_inetbox_ns.class_(
|
||||||
|
"HeaterElecPowerLevelAction", automation.Action)
|
||||||
|
HeaterEnergyMixAction = truma_inetbox_ns.class_(
|
||||||
|
"HeaterEnergyMixAction", automation.Action)
|
||||||
|
TimerDisableAction = truma_inetbox_ns.class_(
|
||||||
|
"TimerDisableAction", automation.Action)
|
||||||
|
TimerActivateAction = truma_inetbox_ns.class_(
|
||||||
|
"TimerActivateAction", automation.Action)
|
||||||
|
WriteTimeAction = truma_inetbox_ns.class_("WriteTimeAction", automation.Action)
|
||||||
|
|
||||||
|
# `EnergyMix` is a enum class and not a namespace but it works.
|
||||||
|
EnergyMix_dummy_ns = truma_inetbox_ns.namespace("EnergyMix")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_ENERGY_MIX = {
|
||||||
|
"NONE": EnergyMix_dummy_ns.ENERGY_MIX_NONE,
|
||||||
|
"GAS": EnergyMix_dummy_ns.ENERGY_MIX_GAS,
|
||||||
|
"ELECTRICITY": EnergyMix_dummy_ns.ENERGY_MIX_ELECTRICITY,
|
||||||
|
"MIX": EnergyMix_dummy_ns.ENERGY_MIX_MIX,
|
||||||
|
}
|
||||||
|
|
||||||
|
# `ElectricPowerLevel` is a enum class and not a namespace but it works.
|
||||||
|
ElectricPowerLevel_dummy_ns = truma_inetbox_ns.namespace("ElectricPowerLevel")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_ELECTRIC_POWER_LEVEL = {
|
||||||
|
"0": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
|
||||||
|
"0W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
|
||||||
|
"0 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
|
||||||
|
"900": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
|
||||||
|
"900W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
|
||||||
|
"900 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
|
||||||
|
"1800": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1800W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1800 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1.8kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1,8kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1.8 kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
"1,8 kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
|
||||||
|
}
|
||||||
|
|
||||||
|
# `HeatingMode` is a enum class and not a namespace but it works.
|
||||||
|
HeatingMode_dummy_ns = truma_inetbox_ns.namespace("HeatingMode")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_HEATING_MODE = {
|
||||||
|
"OFF": HeatingMode_dummy_ns.HEATING_MODE_OFF,
|
||||||
|
"ECO": HeatingMode_dummy_ns.HEATING_MODE_ECO,
|
||||||
|
"HIGH": HeatingMode_dummy_ns.HEATING_MODE_HIGH,
|
||||||
|
"BOOST": HeatingMode_dummy_ns.HEATING_MODE_BOOST,
|
||||||
|
}
|
||||||
|
|
||||||
|
# `TargetTemp` is a enum class and not a namespace but it works.
|
||||||
|
TargetTemp_dummy_ns = truma_inetbox_ns.namespace("TargetTemp")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_WATER_TEMPERATURE = {
|
||||||
|
"OFF": TargetTemp_dummy_ns.TARGET_TEMP_OFF,
|
||||||
|
"ECO": TargetTemp_dummy_ns.TARGET_TEMP_WATER_ECO,
|
||||||
|
"HIGH": TargetTemp_dummy_ns.TARGET_TEMP_WATER_HIGH,
|
||||||
|
"BOOST": TargetTemp_dummy_ns.TARGET_TEMP_WATER_BOOST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.heater.set_target_room_temperature",
|
||||||
|
HeaterRoomTempAction,
|
||||||
|
automation.maybe_conf(
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=30)),
|
||||||
|
cv.Optional(CONF_HEATING_MODE, "OFF"): cv.templatable(cv.enum(CONF_SUPPORTED_HEATING_MODE, upper=True)),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_heater_set_target_room_temperature_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint8)
|
||||||
|
cg.add(var.set_temperature(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_HEATING_MODE], args, cg.uint8)
|
||||||
|
cg.add(var.set_heating_mode(template_))
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.heater.set_target_water_temperature",
|
||||||
|
HeaterWaterTempAction,
|
||||||
|
automation.maybe_conf(
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=80)),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_heater_set_target_water_temperature_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint8)
|
||||||
|
cg.add(var.set_temperature(template_))
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.heater.set_target_water_temperature_enum",
|
||||||
|
HeaterWaterTempEnumAction,
|
||||||
|
automation.maybe_conf(
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.enum(CONF_SUPPORTED_WATER_TEMPERATURE, upper=True))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_heater_set_target_water_temperature_enum_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint16)
|
||||||
|
cg.add(var.set_temperature(template_))
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.heater.set_electric_power_level",
|
||||||
|
HeaterElecPowerLevelAction,
|
||||||
|
automation.maybe_conf(
|
||||||
|
CONF_WATT,
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_WATT): cv.templatable(cv.int_range(min=0, max=1800))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_heater_set_electric_power_level_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
|
||||||
|
cg.add(var.set_watt(template_))
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.heater.set_energy_mix",
|
||||||
|
HeaterEnergyMixAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_ENERGY_MIX): cv.templatable(cv.enum(CONF_SUPPORTED_ENERGY_MIX, upper=True)),
|
||||||
|
cv.Optional(CONF_WATT, 0): cv.templatable(cv.enum(CONF_SUPPORTED_ELECTRIC_POWER_LEVEL, upper=True)),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_heater_set_energy_mix_level_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_ENERGY_MIX], args, cg.uint8)
|
||||||
|
cg.add(var.set_energy_mix(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
|
||||||
|
cg.add(var.set_watt(template_))
|
||||||
|
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.timer.disable",
|
||||||
|
TimerDisableAction,
|
||||||
|
automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_timer_disable_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.timer.activate",
|
||||||
|
TimerActivateAction,
|
||||||
|
automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_START): cv.templatable(cv.int_range(min=0, max=1440)),
|
||||||
|
cv.Required(CONF_STOP): cv.templatable(cv.int_range(min=0, max=1440)),
|
||||||
|
cv.Required(CONF_ROOM_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=30)),
|
||||||
|
cv.Optional(CONF_HEATING_MODE, "OFF"): cv.templatable(cv.enum(CONF_SUPPORTED_HEATING_MODE, upper=True)),
|
||||||
|
cv.Optional(CONF_WATER_TEMPERATURE, 0): cv.templatable(cv.int_range(min=0, max=80)),
|
||||||
|
cv.Optional(CONF_ENERGY_MIX, "NONE"): cv.templatable(cv.enum(CONF_SUPPORTED_ENERGY_MIX, upper=True)),
|
||||||
|
cv.Optional(CONF_WATT, 0): cv.templatable(cv.enum(CONF_SUPPORTED_ELECTRIC_POWER_LEVEL, upper=True)),
|
||||||
|
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_timer_activate_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_START], args, cg.uint16)
|
||||||
|
cg.add(var.set_start(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_STOP], args, cg.uint16)
|
||||||
|
cg.add(var.set_stop(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_ROOM_TEMPERATURE], args, cg.uint8)
|
||||||
|
cg.add(var.set_room_temperature(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_HEATING_MODE], args, cg.uint8)
|
||||||
|
cg.add(var.set_heating_mode(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_WATER_TEMPERATURE], args, cg.uint8)
|
||||||
|
cg.add(var.set_water_temperature(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_ENERGY_MIX], args, cg.uint8)
|
||||||
|
cg.add(var.set_energy_mix(template_))
|
||||||
|
|
||||||
|
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
|
||||||
|
cg.add(var.set_watt(template_))
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"truma_inetbox.clock.set",
|
||||||
|
WriteTimeAction,
|
||||||
|
automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def truma_inetbox_clock_set_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
return var
|
||||||
91
components/truma_inetbox/automation.h
Normal file
91
components/truma_inetbox/automation.h
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
template<typename... Ts> class HeaterRoomTempAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(u_int8_t, temperature)
|
||||||
|
TEMPLATABLE_VALUE(HeatingMode, heating_mode)
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->action_heater_room(this->temperature_.value_or(x..., 0),
|
||||||
|
this->heating_mode_.value_or(x..., HeatingMode::HEATING_MODE_OFF));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HeaterWaterTempAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(u_int8_t, temperature)
|
||||||
|
|
||||||
|
void play(Ts... x) override { this->parent_->action_heater_water(this->temperature_.value_or(x..., 0)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HeaterWaterTempEnumAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(TargetTemp, temperature)
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->action_heater_water(this->temperature_.value_or(x..., TargetTemp::TARGET_TEMP_OFF));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HeaterElecPowerLevelAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(u_int16_t, watt)
|
||||||
|
|
||||||
|
void play(Ts... x) override { this->parent_->action_heater_electric_power_level(this->watt_.value_or(x..., 0)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HeaterEnergyMixAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(EnergyMix, energy_mix)
|
||||||
|
TEMPLATABLE_VALUE(ElectricPowerLevel, watt)
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->action_heater_energy_mix(this->energy_mix_.value_or(x..., EnergyMix::ENERGY_MIX_GAS),
|
||||||
|
this->watt_.value_or(x..., ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class TimerDisableAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void play(Ts... x) override { this->parent_->action_timer_disable(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class TimerActivateAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(u_int16_t, start)
|
||||||
|
TEMPLATABLE_VALUE(u_int16_t, stop)
|
||||||
|
TEMPLATABLE_VALUE(u_int8_t, room_temperature)
|
||||||
|
TEMPLATABLE_VALUE(HeatingMode, heating_mode)
|
||||||
|
TEMPLATABLE_VALUE(u_int8_t, water_temperature)
|
||||||
|
TEMPLATABLE_VALUE(EnergyMix, energy_mix)
|
||||||
|
TEMPLATABLE_VALUE(ElectricPowerLevel, watt)
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->action_timer_activate(
|
||||||
|
this->start_.value(x...), this->stop_.value(x...), this->room_temperature_.value(x...),
|
||||||
|
this->heating_mode_.value_or(x..., HeatingMode::HEATING_MODE_OFF), this->water_temperature_.value_or(x..., 0),
|
||||||
|
this->energy_mix_.value_or(x..., EnergyMix::ENERGY_MIX_NONE),
|
||||||
|
this->watt_.value_or(x..., ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class WriteTimeAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void play(Ts... x) override { this->parent_->action_write_time(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrumaiNetBoxAppHeaterMessageTrigger : public Trigger<const StatusFrameHeater *> {
|
||||||
|
public:
|
||||||
|
explicit TrumaiNetBoxAppHeaterMessageTrigger(TrumaiNetBoxApp *parent) {
|
||||||
|
parent->add_on_heater_message_callback([this](const StatusFrameHeater *message) { this->trigger(message); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
#include "TrumaCpPlusBinarySensor.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.binary_sensor";
|
||||||
|
|
||||||
|
void TrumaCpPlusBinarySensor::update() {
|
||||||
|
if (this->parent_->get_lin_bus_fault() || (this->parent_->get_last_cp_plus_request() == 0)) {
|
||||||
|
this->publish_state(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto timeout = this->parent_->get_last_cp_plus_request() + 30 * 1000 * 1000 /* 30 seconds*/;
|
||||||
|
this->publish_state(esp_timer_get_time() < timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaCpPlusBinarySensor::dump_config() { ESP_LOGCONFIG("", "Truma CP Plus Binary Sensor"); }
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
class TrumaCpPlusBinarySensor : public PollingComponent,
|
||||||
|
public binary_sensor::BinarySensor,
|
||||||
|
public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void update() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
#include "TrumaHeaterBinarySensor.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.binary_sensor";
|
||||||
|
|
||||||
|
void TrumaHeaterBinarySensor::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
|
||||||
|
switch (this->type_) {
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_ROOM:
|
||||||
|
this->publish_state(status_heater->target_temp_room != TargetTemp::TARGET_TEMP_OFF);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_WATER:
|
||||||
|
this->publish_state(status_heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_GAS:
|
||||||
|
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_GAS);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_MIX_1:
|
||||||
|
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_MIX &&
|
||||||
|
status_heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_MIX_2:
|
||||||
|
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_MIX &&
|
||||||
|
status_heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::HEATER_ELECTRICITY:
|
||||||
|
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_ELECTRICITY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaHeaterBinarySensor::dump_config() {
|
||||||
|
ESP_LOGCONFIG("", "Truma Heater Binary Sensor");
|
||||||
|
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
#include "enum.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
class TrumaHeaterBinarySensor : public Component, public binary_sensor::BinarySensor, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void set_type(TRUMA_BINARY_SENSOR_TYPE val) { this->type_ = val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TRUMA_BINARY_SENSOR_TYPE type_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
#include "TrumaTimerBinarySensor.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.binary_sensor";
|
||||||
|
|
||||||
|
void TrumaTimerBinarySensor::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameTimer *status_timer) {
|
||||||
|
switch (this->type_) {
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::TIMER_ACTIVE:
|
||||||
|
this->publish_state(status_timer->timer_active == TimerActive::TIMER_ACTIVE_ON);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::TIMER_ROOM:
|
||||||
|
this->publish_state(status_timer->timer_target_temp_room != TargetTemp::TARGET_TEMP_OFF);
|
||||||
|
break;
|
||||||
|
case TRUMA_BINARY_SENSOR_TYPE::TIMER_WATER:
|
||||||
|
this->publish_state(status_timer->timer_target_temp_water != TargetTemp::TARGET_TEMP_OFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaTimerBinarySensor::dump_config() {
|
||||||
|
ESP_LOGCONFIG("", "Truma Timer Binary Sensor");
|
||||||
|
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
#include "enum.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
class TrumaTimerBinarySensor : public Component, public binary_sensor::BinarySensor, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void set_type(TRUMA_BINARY_SENSOR_TYPE val) { this->type_ = val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TRUMA_BINARY_SENSOR_TYPE type_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
70
components/truma_inetbox/binary_sensor/__init__.py
Normal file
70
components/truma_inetbox/binary_sensor/__init__.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from esphome.components import binary_sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
CONF_UPDATE_INTERVAL
|
||||||
|
)
|
||||||
|
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
|
||||||
|
|
||||||
|
DEPENDENCIES = ["truma_inetbox"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
TrumaSensor = truma_inetbox_ns.class_(
|
||||||
|
"TrumaBinarySensor", binary_sensor.BinarySensor, cg.Component)
|
||||||
|
|
||||||
|
# `TRUMA_BINARY_SENSOR_TYPE` is a enum class and not a namespace but it works.
|
||||||
|
TRUMA_BINARY_SENSOR_TYPE_dummy_ns = truma_inetbox_ns.namespace(
|
||||||
|
"TRUMA_BINARY_SENSOR_TYPE")
|
||||||
|
|
||||||
|
# 0 - C++ class
|
||||||
|
# 1 - C++ enum
|
||||||
|
CONF_SUPPORTED_TYPE = {
|
||||||
|
# TrumaCpPlusBinarySensor
|
||||||
|
"CP_PLUS_CONNECTED": (truma_inetbox_ns.class_("TrumaCpPlusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent), None),
|
||||||
|
# TrumaHeaterBinarySensor
|
||||||
|
"HEATER_ROOM": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_ROOM),
|
||||||
|
"HEATER_WATER": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_WATER),
|
||||||
|
"HEATER_GAS": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_GAS),
|
||||||
|
"HEATER_MIX_1": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_MIX_1),
|
||||||
|
"HEATER_MIX_2": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_MIX_2),
|
||||||
|
"HEATER_ELECTRICITY": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_ELECTRICITY),
|
||||||
|
# TrumaTimerBinarySensor
|
||||||
|
"TIMER_ACTIVE": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_ACTIVE),
|
||||||
|
"TIMER_ROOM": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_ROOM),
|
||||||
|
"TIMER_WATER": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_WATER),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_based_on_type():
|
||||||
|
def set_defaults_(config):
|
||||||
|
# Update type based on configuration
|
||||||
|
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][0]
|
||||||
|
|
||||||
|
# set defaults based on sensor type:
|
||||||
|
if config[CONF_TYPE] == "CP_PLUS_CONNECTED":
|
||||||
|
if CONF_UPDATE_INTERVAL not in config:
|
||||||
|
config[CONF_UPDATE_INTERVAL] = 500 # 0.5 seconds
|
||||||
|
return config
|
||||||
|
|
||||||
|
return set_defaults_
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
binary_sensor.binary_sensor_schema(TrumaSensor)
|
||||||
|
.extend({
|
||||||
|
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
|
||||||
|
}).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await binary_sensor.new_binary_sensor(config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
|
||||||
|
|
||||||
|
if CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]:
|
||||||
|
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]))
|
||||||
20
components/truma_inetbox/binary_sensor/enum.h
Normal file
20
components/truma_inetbox/binary_sensor/enum.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
enum class TRUMA_BINARY_SENSOR_TYPE {
|
||||||
|
HEATER_ROOM,
|
||||||
|
HEATER_WATER,
|
||||||
|
HEATER_GAS,
|
||||||
|
HEATER_MIX_1,
|
||||||
|
HEATER_MIX_2,
|
||||||
|
HEATER_ELECTRICITY,
|
||||||
|
|
||||||
|
TIMER_ACTIVE,
|
||||||
|
TIMER_ROOM,
|
||||||
|
TIMER_WATER,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
105
components/truma_inetbox/climate/TrumaRoomClimate.cpp
Normal file
105
components/truma_inetbox/climate/TrumaRoomClimate.cpp
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#include "TrumaRoomClimate.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.truma_room_climate";
|
||||||
|
void TrumaRoomClimate::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
|
||||||
|
// Publish updated state
|
||||||
|
this->target_temperature = temp_code_to_decimal(status_heater->target_temp_room);
|
||||||
|
this->current_temperature = temp_code_to_decimal(status_heater->current_temp_room);
|
||||||
|
this->mode = (status_heater->operating_status >= OperatingStatus::OPERATING_STATUS_START_OR_COOL_DOWN)
|
||||||
|
? climate::CLIMATE_MODE_HEAT
|
||||||
|
: climate::CLIMATE_MODE_OFF;
|
||||||
|
|
||||||
|
switch (status_heater->heating_mode) {
|
||||||
|
case HeatingMode::HEATING_MODE_ECO:
|
||||||
|
this->preset = climate::CLIMATE_PRESET_ECO;
|
||||||
|
break;
|
||||||
|
case HeatingMode::HEATING_MODE_HIGH:
|
||||||
|
this->preset = climate::CLIMATE_PRESET_COMFORT;
|
||||||
|
break;
|
||||||
|
case HeatingMode::HEATING_MODE_BOOST:
|
||||||
|
this->preset = climate::CLIMATE_PRESET_BOOST;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->publish_state();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaRoomClimate::dump_config() { ESP_LOGCONFIG(TAG, "Truma Room Climate"); }
|
||||||
|
|
||||||
|
void TrumaRoomClimate::control(const climate::ClimateCall &call) {
|
||||||
|
if (call.get_target_temperature().has_value()) {
|
||||||
|
float temp = *call.get_target_temperature();
|
||||||
|
this->parent_->action_heater_room(static_cast<u_int8_t>(temp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call.get_mode().has_value()) {
|
||||||
|
// User requested mode change
|
||||||
|
climate::ClimateMode mode = *call.get_mode();
|
||||||
|
auto status_heater = this->parent_->get_status_heater();
|
||||||
|
switch (mode) {
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
if (status_heater->target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
|
||||||
|
this->parent_->action_heater_room(5);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->parent_->action_heater_room(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call.get_preset().has_value()) {
|
||||||
|
climate::ClimatePreset pres = *call.get_preset();
|
||||||
|
auto status_heater = this->parent_->get_status_heater();
|
||||||
|
auto current_target_temp = temp_code_to_decimal(status_heater->target_temp_room);
|
||||||
|
if (call.get_target_temperature().has_value()) {
|
||||||
|
current_target_temp = *call.get_target_temperature();
|
||||||
|
}
|
||||||
|
switch (pres) {
|
||||||
|
case climate::CLIMATE_PRESET_ECO:
|
||||||
|
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_ECO);
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_PRESET_COMFORT:
|
||||||
|
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_HIGH);
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_PRESET_BOOST:
|
||||||
|
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_BOOST);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->parent_->action_heater_room(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
climate::ClimateTraits TrumaRoomClimate::traits() {
|
||||||
|
// The capabilities of the climate device
|
||||||
|
auto traits = climate::ClimateTraits();
|
||||||
|
traits.set_supports_current_temperature(true);
|
||||||
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT});
|
||||||
|
// traits.set_supported_fan_modes({{
|
||||||
|
// climate::CLIMATE_FAN_LOW,
|
||||||
|
// climate::CLIMATE_FAN_MEDIUM,
|
||||||
|
// climate::CLIMATE_FAN_HIGH,
|
||||||
|
// }});
|
||||||
|
traits.set_supported_presets({{
|
||||||
|
climate::CLIMATE_PRESET_NONE,
|
||||||
|
climate::CLIMATE_PRESET_ECO,
|
||||||
|
climate::CLIMATE_PRESET_COMFORT,
|
||||||
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
|
}});
|
||||||
|
traits.set_visual_min_temperature(5);
|
||||||
|
traits.set_visual_max_temperature(30);
|
||||||
|
traits.set_visual_temperature_step(1);
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
22
components/truma_inetbox/climate/TrumaRoomClimate.h
Normal file
22
components/truma_inetbox/climate/TrumaRoomClimate.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
class TrumaRoomClimate : public Component, public climate::Climate, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void control(const climate::ClimateCall &call) override;
|
||||||
|
|
||||||
|
climate::ClimateTraits traits() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
54
components/truma_inetbox/climate/TrumaWaterClimate.cpp
Normal file
54
components/truma_inetbox/climate/TrumaWaterClimate.cpp
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#include "TrumaWaterClimate.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.truma_water_climate";
|
||||||
|
void TrumaWaterClimate::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
|
||||||
|
// Publish updated state
|
||||||
|
this->target_temperature = temp_code_to_decimal(status_heater->target_temp_water);
|
||||||
|
this->current_temperature = temp_code_to_decimal(status_heater->current_temp_water);
|
||||||
|
this->mode = (status_heater->target_temp_water == TargetTemp::TARGET_TEMP_OFF) ? climate::CLIMATE_MODE_OFF
|
||||||
|
: climate::CLIMATE_MODE_HEAT;
|
||||||
|
this->publish_state();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaWaterClimate::dump_config() { ESP_LOGCONFIG(TAG, "Truma Climate"); }
|
||||||
|
|
||||||
|
void TrumaWaterClimate::control(const climate::ClimateCall &call) {
|
||||||
|
if (call.get_target_temperature().has_value()) {
|
||||||
|
float temp = *call.get_target_temperature();
|
||||||
|
this->parent_->action_heater_water(static_cast<u_int8_t>(temp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call.get_mode().has_value()) {
|
||||||
|
climate::ClimateMode mode = *call.get_mode();
|
||||||
|
auto status_heater = this->parent_->get_status_heater();
|
||||||
|
switch (mode) {
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
if (status_heater->target_temp_water == TargetTemp::TARGET_TEMP_OFF) {
|
||||||
|
this->parent_->action_heater_water(40);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->parent_->action_heater_water(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
climate::ClimateTraits TrumaWaterClimate::traits() {
|
||||||
|
// The capabilities of the climate device
|
||||||
|
auto traits = climate::ClimateTraits();
|
||||||
|
traits.set_supports_current_temperature(true);
|
||||||
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT});
|
||||||
|
traits.set_visual_min_temperature(40);
|
||||||
|
traits.set_visual_max_temperature(80);
|
||||||
|
traits.set_visual_temperature_step(20);
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
22
components/truma_inetbox/climate/TrumaWaterClimate.h
Normal file
22
components/truma_inetbox/climate/TrumaWaterClimate.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
class TrumaWaterClimate : public Component, public climate::Climate, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void control(const climate::ClimateCall &call) override;
|
||||||
|
|
||||||
|
climate::ClimateTraits traits() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
45
components/truma_inetbox/climate/__init__.py
Normal file
45
components/truma_inetbox/climate/__init__.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from esphome.components import climate
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
)
|
||||||
|
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
|
||||||
|
|
||||||
|
DEPENDENCIES = ["truma_inetbox"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
TrumaClimate = truma_inetbox_ns.class_(
|
||||||
|
"TrumaClimate", climate.Climate, cg.Component)
|
||||||
|
|
||||||
|
CONF_SUPPORTED_TYPE = {
|
||||||
|
"ROOM": truma_inetbox_ns.class_("TrumaRoomClimate", climate.Climate, cg.Component),
|
||||||
|
"WATER": truma_inetbox_ns.class_("TrumaWaterClimate", climate.Climate, cg.Component),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_based_on_type():
|
||||||
|
def set_defaults_(config):
|
||||||
|
# update the class
|
||||||
|
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]]
|
||||||
|
return config
|
||||||
|
|
||||||
|
return set_defaults_
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TrumaClimate),
|
||||||
|
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await climate.register_climate(var, config)
|
||||||
|
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
|
||||||
65
components/truma_inetbox/entity_helpers.py
Normal file
65
components/truma_inetbox/entity_helpers.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import esphome.final_validate as fv
|
||||||
|
from esphome import core
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
def count_id_usage(property_to_update, property_to_count, property_value):
|
||||||
|
"""Validator that counts a configuration property from another entity, for use with FINAL_VALIDATE_SCHEMA.
|
||||||
|
If a property is already set, it will not be updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _walk_config(config, path):
|
||||||
|
walk = [path] if not isinstance(path, list) else path
|
||||||
|
for item_or_index in walk:
|
||||||
|
config = config[item_or_index]
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _count_config_value(config, conf_name_list, conf_value):
|
||||||
|
ret = 0
|
||||||
|
|
||||||
|
if isinstance(config, (list, tuple)):
|
||||||
|
for config_item in config:
|
||||||
|
ret += _count_config_value(config_item,
|
||||||
|
conf_name_list, conf_value)
|
||||||
|
for conf_name in conf_name_list:
|
||||||
|
if conf_name in config and isinstance(config, (tuple)) and config[0] == conf_name and isinstance(config[1], (core.ID)) and config[1].type is conf_value:
|
||||||
|
ret += 1
|
||||||
|
elif isinstance(config, (dict)):
|
||||||
|
for config_item in config.items():
|
||||||
|
ret += _count_config_value(config_item,
|
||||||
|
conf_name_list, conf_value)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def inherit_property(config):
|
||||||
|
# Ensure `property_to_count` is a list
|
||||||
|
property_to_count_list = [property_to_count] if not isinstance(
|
||||||
|
property_to_count, list) else property_to_count
|
||||||
|
|
||||||
|
# Split the property into its path and name
|
||||||
|
if not isinstance(property_to_update, list):
|
||||||
|
property_path, property = [], property_to_update
|
||||||
|
else:
|
||||||
|
property_path, property = property_to_update[:-
|
||||||
|
1], property_to_update[-1]
|
||||||
|
|
||||||
|
# Check if the property is accessible
|
||||||
|
try:
|
||||||
|
config_part = _walk_config(config, property_path)
|
||||||
|
except KeyError:
|
||||||
|
return config
|
||||||
|
|
||||||
|
# Only update the property if it does not exist yet
|
||||||
|
if property not in config_part:
|
||||||
|
fconf = fv.full_config.get()
|
||||||
|
|
||||||
|
count = _count_config_value(
|
||||||
|
fconf, property_to_count_list, property_value)
|
||||||
|
|
||||||
|
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
|
||||||
|
this_config = _walk_config(
|
||||||
|
fconf.get_config_for_path(path), property_path
|
||||||
|
)
|
||||||
|
this_config[property] = count
|
||||||
|
return config
|
||||||
|
|
||||||
|
return inherit_property
|
||||||
107
components/truma_inetbox/helpers.cpp
Normal file
107
components/truma_inetbox/helpers.cpp
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
#include "helpers.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
u_int8_t addr_parity(const u_int8_t PID) {
|
||||||
|
u_int8_t P0 = ((PID >> 0) + (PID >> 1) + (PID >> 2) + (PID >> 4)) & 1;
|
||||||
|
u_int8_t P1 = ~((PID >> 1) + (PID >> 3) + (PID >> 4) + (PID >> 5)) & 1;
|
||||||
|
return (P0 | (P1 << 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sum = 0 LIN 1.X CRC, sum = PID LIN 2.X CRC Enhanced
|
||||||
|
u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum) {
|
||||||
|
for (u_int8_t i = 0; i < length; i++) {
|
||||||
|
sum += message[i];
|
||||||
|
|
||||||
|
if (sum >= 256)
|
||||||
|
sum -= 255;
|
||||||
|
}
|
||||||
|
return (~sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
float temp_code_to_decimal(u_int16_t val, float zero) {
|
||||||
|
if (val == 0) {
|
||||||
|
return zero;
|
||||||
|
}
|
||||||
|
return ((float) val) / 10.0f - 273.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float temp_code_to_decimal(TargetTemp val, float zero) { return temp_code_to_decimal((u_int16_t) val, zero); }
|
||||||
|
|
||||||
|
TargetTemp decimal_to_room_temp(u_int8_t val) {
|
||||||
|
if (val == 0) {
|
||||||
|
return TargetTemp::TARGET_TEMP_OFF;
|
||||||
|
}
|
||||||
|
if (val <= 5) {
|
||||||
|
return TargetTemp::TARGET_TEMP_ROOM_MIN;
|
||||||
|
}
|
||||||
|
if (val >= 30) {
|
||||||
|
return TargetTemp::TARGET_TEMP_ROOM_MAX;
|
||||||
|
}
|
||||||
|
return (TargetTemp) ((((u_int16_t) val) + 273) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetTemp decimal_to_room_temp(float val) {
|
||||||
|
if (val == NAN) {
|
||||||
|
return TargetTemp::TARGET_TEMP_OFF;
|
||||||
|
}
|
||||||
|
if (val <= 5) {
|
||||||
|
return TargetTemp::TARGET_TEMP_ROOM_MIN;
|
||||||
|
}
|
||||||
|
if (val >= 30) {
|
||||||
|
return TargetTemp::TARGET_TEMP_ROOM_MAX;
|
||||||
|
}
|
||||||
|
return (TargetTemp) ((val + 273) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetTemp deciaml_to_water_temp(u_int8_t val) {
|
||||||
|
if (val < 40) {
|
||||||
|
return TargetTemp::TARGET_TEMP_OFF;
|
||||||
|
} else if (val >= 40 && val < 60) {
|
||||||
|
return TargetTemp::TARGET_TEMP_WATER_ECO;
|
||||||
|
} else if (val >= 60 && val < 80) {
|
||||||
|
return TargetTemp::TARGET_TEMP_WATER_HIGH;
|
||||||
|
} else {
|
||||||
|
return TargetTemp::TARGET_TEMP_WATER_BOOST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float offset_code_to_decimal(TempOffset val) { return ((float) val) / 10.0f - 17.0f; }
|
||||||
|
|
||||||
|
const std::string operating_status_to_str(OperatingStatus val) {
|
||||||
|
if (val == OperatingStatus::OPERATING_STATUS_OFF) {
|
||||||
|
return "OFF";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_WARNING) {
|
||||||
|
return "WARNING";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_START_OR_COOL_DOWN) {
|
||||||
|
return "START/COOL DOWN";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_ON_5) {
|
||||||
|
return "ON (5)";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_ON_6) {
|
||||||
|
return "ON (6)";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_ON_7) {
|
||||||
|
return "ON (7)";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_ON_8) {
|
||||||
|
return "ON (8)";
|
||||||
|
} else if (val == OperatingStatus::OPERATING_STATUS_ON_9) {
|
||||||
|
return "ON (9)";
|
||||||
|
} else {
|
||||||
|
return str_snprintf("ON %x", (uint8_t) val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectricPowerLevel decimal_to_el_power_level(u_int16_t val) {
|
||||||
|
if (val >= 1800) {
|
||||||
|
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800;
|
||||||
|
} else if (val >= 900) {
|
||||||
|
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
|
||||||
|
} else {
|
||||||
|
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
23
components/truma_inetbox/helpers.h
Normal file
23
components/truma_inetbox/helpers.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
// First byte is service identifier and to be ignored.
|
||||||
|
const std::array<u_int8_t, 11> truma_message_header = {0x00, 0x00, 0x1F, 0x00, 0x1E, 0x00,
|
||||||
|
0x00, 0x22, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
|
u_int8_t addr_parity(const u_int8_t pid);
|
||||||
|
u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum);
|
||||||
|
float temp_code_to_decimal(u_int16_t val, float zero = NAN);
|
||||||
|
float temp_code_to_decimal(TargetTemp val, float zero = NAN);
|
||||||
|
TargetTemp decimal_to_room_temp(u_int8_t val);
|
||||||
|
TargetTemp decimal_to_room_temp(float val);
|
||||||
|
TargetTemp deciaml_to_water_temp(u_int8_t val);
|
||||||
|
float offset_code_to_decimal(TempOffset val);
|
||||||
|
const std::string operating_status_to_str(OperatingStatus val);
|
||||||
|
ElectricPowerLevel decimal_to_el_power_level(u_int16_t val);
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
45
components/truma_inetbox/number/TrumaHeaterNumber.cpp
Normal file
45
components/truma_inetbox/number/TrumaHeaterNumber.cpp
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#include "TrumaHeaterNumber.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.sensor";
|
||||||
|
|
||||||
|
void TrumaHeaterNumber::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
|
||||||
|
switch (this->type_) {
|
||||||
|
case TRUMA_NUMBER_TYPE::TARGET_ROOM_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->target_temp_room, 0));
|
||||||
|
break;
|
||||||
|
case TRUMA_NUMBER_TYPE::TARGET_WATER_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->target_temp_water, 0));
|
||||||
|
break;
|
||||||
|
case TRUMA_NUMBER_TYPE::ELECTRIC_POWER_LEVEL:
|
||||||
|
this->publish_state(static_cast<float>(status_heater->el_power_level_a));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaHeaterNumber::control(float value) {
|
||||||
|
switch (this->type_) {
|
||||||
|
case TRUMA_NUMBER_TYPE::TARGET_ROOM_TEMPERATURE:
|
||||||
|
this->parent_->action_heater_room(static_cast<u_int8_t>(value));
|
||||||
|
break;
|
||||||
|
case TRUMA_NUMBER_TYPE::TARGET_WATER_TEMPERATURE:
|
||||||
|
this->parent_->action_heater_water(static_cast<u_int8_t>(value));
|
||||||
|
break;
|
||||||
|
case TRUMA_NUMBER_TYPE::ELECTRIC_POWER_LEVEL:
|
||||||
|
this->parent_->action_heater_water(static_cast<u_int16_t>(value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaHeaterNumber::dump_config() {
|
||||||
|
ESP_LOGCONFIG("", "Truma Heater Number");
|
||||||
|
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
30
components/truma_inetbox/number/TrumaHeaterNumber.h
Normal file
30
components/truma_inetbox/number/TrumaHeaterNumber.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/number/number.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
enum class TRUMA_NUMBER_TYPE {
|
||||||
|
TARGET_ROOM_TEMPERATURE,
|
||||||
|
TARGET_WATER_TEMPERATURE,
|
||||||
|
ELECTRIC_POWER_LEVEL,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrumaHeaterNumber : public Component, public number::Number, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void set_type(TRUMA_NUMBER_TYPE val) { this->type_ = val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TRUMA_NUMBER_TYPE type_;
|
||||||
|
|
||||||
|
void control(float value) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
119
components/truma_inetbox/number/__init__.py
Normal file
119
components/truma_inetbox/number/__init__.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from esphome.components import number
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
CONF_ICON,
|
||||||
|
ICON_THERMOMETER,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
UNIT_WATT,
|
||||||
|
ICON_POWER,
|
||||||
|
CONF_MAX_VALUE,
|
||||||
|
CONF_MIN_VALUE,
|
||||||
|
CONF_STEP,
|
||||||
|
)
|
||||||
|
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
|
||||||
|
|
||||||
|
DEPENDENCIES = ["truma_inetbox"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
CONF_CLASS = "class"
|
||||||
|
|
||||||
|
TrumaNumber = truma_inetbox_ns.class_(
|
||||||
|
"TrumaNumber", number.Number, cg.Component)
|
||||||
|
|
||||||
|
# `TRUMA_NUMBER_TYPE` is a enum class and not a namespace but it works.
|
||||||
|
TRUMA_NUMBER_TYPE_dummy_ns = truma_inetbox_ns.namespace("TRUMA_NUMBER_TYPE")
|
||||||
|
|
||||||
|
CONF_SUPPORTED_TYPE = {
|
||||||
|
"TARGET_ROOM_TEMPERATURE": {
|
||||||
|
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
|
||||||
|
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.TARGET_ROOM_TEMPERATURE,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
|
||||||
|
CONF_ICON: ICON_THERMOMETER,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
CONF_MAX_VALUE: 30,
|
||||||
|
# Values between 0 and 5 are handeld as off.
|
||||||
|
CONF_MIN_VALUE: 0,
|
||||||
|
CONF_STEP: 1,
|
||||||
|
},
|
||||||
|
"TARGET_WATER_TEMPERATURE": {
|
||||||
|
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
|
||||||
|
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.TARGET_WATER_TEMPERATURE,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
|
||||||
|
CONF_ICON: ICON_THERMOMETER,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
CONF_MAX_VALUE: 80,
|
||||||
|
# Values between 0 and 40 are handeld as off.
|
||||||
|
CONF_MIN_VALUE: 0,
|
||||||
|
CONF_STEP: 20,
|
||||||
|
},
|
||||||
|
"ELECTRIC_POWER_LEVEL": {
|
||||||
|
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
|
||||||
|
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.ELECTRIC_POWER_LEVEL,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT,
|
||||||
|
CONF_ICON: ICON_POWER,
|
||||||
|
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||||
|
CONF_MAX_VALUE: 1800,
|
||||||
|
CONF_MIN_VALUE: 0,
|
||||||
|
CONF_STEP: 900,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_based_on_type():
|
||||||
|
def set_defaults_(config):
|
||||||
|
# update the class
|
||||||
|
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_CLASS]
|
||||||
|
# set defaults based on sensor type:
|
||||||
|
if CONF_UNIT_OF_MEASUREMENT not in config:
|
||||||
|
config[CONF_UNIT_OF_MEASUREMENT] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
|
||||||
|
][CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
if CONF_ICON not in config:
|
||||||
|
config[CONF_ICON] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_ICON]
|
||||||
|
if CONF_DEVICE_CLASS not in config:
|
||||||
|
config[CONF_DEVICE_CLASS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
|
||||||
|
][CONF_DEVICE_CLASS]
|
||||||
|
if CONF_MAX_VALUE not in config:
|
||||||
|
config[CONF_MAX_VALUE] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
|
||||||
|
][CONF_MAX_VALUE]
|
||||||
|
if CONF_MIN_VALUE not in config:
|
||||||
|
config[CONF_MIN_VALUE] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
|
||||||
|
][CONF_MIN_VALUE]
|
||||||
|
if CONF_STEP not in config:
|
||||||
|
config[CONF_STEP] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_STEP]
|
||||||
|
return config
|
||||||
|
|
||||||
|
return set_defaults_
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = number.NUMBER_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TrumaNumber),
|
||||||
|
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
|
||||||
|
cv.Optional(CONF_MAX_VALUE): cv.float_,
|
||||||
|
cv.Optional(CONF_MIN_VALUE): cv.float_,
|
||||||
|
cv.Optional(CONF_STEP): cv.positive_float,
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await number.register_number(
|
||||||
|
var,
|
||||||
|
config,
|
||||||
|
min_value=config[CONF_MIN_VALUE],
|
||||||
|
max_value=config[CONF_MAX_VALUE],
|
||||||
|
step=config[CONF_STEP],
|
||||||
|
)
|
||||||
|
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
|
||||||
|
|
||||||
|
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_TYPE]))
|
||||||
46
components/truma_inetbox/sensor/TrumaSensor.cpp
Normal file
46
components/truma_inetbox/sensor/TrumaSensor.cpp
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#include "TrumaSensor.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.sensor";
|
||||||
|
|
||||||
|
void TrumaSensor::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
|
||||||
|
switch (this->type_) {
|
||||||
|
case TRUMA_SENSOR_TYPE::CURRENT_ROOM_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->current_temp_room));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::CURRENT_WATER_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->current_temp_water));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::TARGET_ROOM_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->target_temp_room));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::TARGET_WATER_TEMPERATURE:
|
||||||
|
this->publish_state(temp_code_to_decimal(status_heater->target_temp_water));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::HEATING_MODE:
|
||||||
|
this->publish_state(static_cast<float>(status_heater->heating_mode));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::ELECTRIC_POWER_LEVEL:
|
||||||
|
this->publish_state(static_cast<float>(status_heater->el_power_level_a));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::ENERGY_MIX:
|
||||||
|
this->publish_state(static_cast<float>(status_heater->energy_mix_a));
|
||||||
|
break;
|
||||||
|
case TRUMA_SENSOR_TYPE::OPERATING_STATUS:
|
||||||
|
this->publish_state(static_cast<float>(status_heater->operating_status));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaSensor::dump_config() {
|
||||||
|
LOG_SENSOR("", "Truma Sensor", this);
|
||||||
|
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
|
||||||
|
}
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
32
components/truma_inetbox/sensor/TrumaSensor.h
Normal file
32
components/truma_inetbox/sensor/TrumaSensor.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
enum class TRUMA_SENSOR_TYPE {
|
||||||
|
CURRENT_ROOM_TEMPERATURE,
|
||||||
|
CURRENT_WATER_TEMPERATURE,
|
||||||
|
TARGET_ROOM_TEMPERATURE,
|
||||||
|
TARGET_WATER_TEMPERATURE,
|
||||||
|
HEATING_MODE,
|
||||||
|
ELECTRIC_POWER_LEVEL,
|
||||||
|
ENERGY_MIX,
|
||||||
|
OPERATING_STATUS,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrumaSensor : public Component, public sensor::Sensor, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void set_type(TRUMA_SENSOR_TYPE val) { this->type_ = val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
TRUMA_SENSOR_TYPE type_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
82
components/truma_inetbox/sensor/__init__.py
Normal file
82
components/truma_inetbox/sensor/__init__.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from esphome.components import sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
CONF_ICON,
|
||||||
|
ICON_THERMOMETER,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
CONF_ACCURACY_DECIMALS,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
UNIT_WATT,
|
||||||
|
UNIT_EMPTY,
|
||||||
|
ICON_GAS_CYLINDER,
|
||||||
|
ICON_POWER,
|
||||||
|
)
|
||||||
|
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
|
||||||
|
|
||||||
|
DEPENDENCIES = ["truma_inetbox"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
TrumaSensor = truma_inetbox_ns.class_(
|
||||||
|
"TrumaSensor", sensor.Sensor, cg.Component)
|
||||||
|
|
||||||
|
# `TRUMA_SENSOR_TYPE` is a enum class and not a namespace but it works.
|
||||||
|
TRUMA_SENSOR_TYPE_dummy_ns = truma_inetbox_ns.namespace("TRUMA_SENSOR_TYPE")
|
||||||
|
|
||||||
|
# 0 - C++ enum
|
||||||
|
# 1 - CONF_UNIT_OF_MEASUREMENT
|
||||||
|
# 2 - CONF_ICON
|
||||||
|
# 3 - CONF_ACCURACY_DECIMALS
|
||||||
|
# 4 - CONF_DEVICE_CLASS
|
||||||
|
CONF_SUPPORTED_TYPE = {
|
||||||
|
"CURRENT_ROOM_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.CURRENT_ROOM_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"CURRENT_WATER_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.CURRENT_WATER_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"TARGET_ROOM_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.TARGET_ROOM_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"TARGET_WATER_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.TARGET_WATER_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"HEATING_MODE": (TRUMA_SENSOR_TYPE_dummy_ns.HEATING_MODE, UNIT_EMPTY, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"ELECTRIC_POWER_LEVEL": (TRUMA_SENSOR_TYPE_dummy_ns.ELECTRIC_POWER_LEVEL, UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"ENERGY_MIX": (TRUMA_SENSOR_TYPE_dummy_ns.ENERGY_MIX, UNIT_EMPTY, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
"OPERATING_STATUS": (TRUMA_SENSOR_TYPE_dummy_ns.OPERATING_STATUS, UNIT_EMPTY, ICON_POWER, 0, DEVICE_CLASS_TEMPERATURE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_based_on_type():
|
||||||
|
def set_defaults_(config):
|
||||||
|
# set defaults based on sensor type:
|
||||||
|
if CONF_UNIT_OF_MEASUREMENT not in config:
|
||||||
|
config[CONF_UNIT_OF_MEASUREMENT] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]
|
||||||
|
if CONF_ICON not in config:
|
||||||
|
config[CONF_ICON] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][2]
|
||||||
|
if CONF_ACCURACY_DECIMALS not in config:
|
||||||
|
config[CONF_ACCURACY_DECIMALS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][3]
|
||||||
|
if CONF_DEVICE_CLASS not in config:
|
||||||
|
config[CONF_DEVICE_CLASS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][4]
|
||||||
|
return config
|
||||||
|
|
||||||
|
return set_defaults_
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TrumaSensor),
|
||||||
|
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await sensor.register_sensor(var, config)
|
||||||
|
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
|
||||||
|
|
||||||
|
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][0]))
|
||||||
47
components/truma_inetbox/time/TrumaTime.cpp
Normal file
47
components/truma_inetbox/time/TrumaTime.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#include "TrumaTime.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/truma_inetbox/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
static const char *const TAG = "truma_inetbox.time";
|
||||||
|
|
||||||
|
void TrumaTime::setup() {
|
||||||
|
this->parent_->register_listener([this](const StatusFrameClock *status_clock) {
|
||||||
|
if (this->auto_disable_count_ > 0) {
|
||||||
|
if (this->read_time() && this->auto_disable_) {
|
||||||
|
this->auto_disable_count_--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrumaTime::update() {}
|
||||||
|
|
||||||
|
void TrumaTime::dump_config() { ESP_LOGCONFIG(TAG, "Truma Time", this); }
|
||||||
|
|
||||||
|
bool TrumaTime::read_time() {
|
||||||
|
if (!this->parent_->get_status_clock_valid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto status_clock = this->parent_->get_status_clock();
|
||||||
|
|
||||||
|
time::ESPTime rtc_time{.second = status_clock->clock_second,
|
||||||
|
.minute = status_clock->clock_minute,
|
||||||
|
.hour = status_clock->clock_hour,
|
||||||
|
.day_of_week = 1,
|
||||||
|
.day_of_month = 1,
|
||||||
|
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
|
||||||
|
.month = 1,
|
||||||
|
.year = 2020};
|
||||||
|
if (!rtc_time.is_valid()) {
|
||||||
|
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
26
components/truma_inetbox/time/TrumaTime.h
Normal file
26
components/truma_inetbox/time/TrumaTime.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/time/real_time_clock.h"
|
||||||
|
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace truma_inetbox {
|
||||||
|
|
||||||
|
class TrumaTime : public time::RealTimeClock, public Parented<TrumaiNetBoxApp> {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void update() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
bool read_time();
|
||||||
|
|
||||||
|
void set_auto_disable(bool val) { this->auto_disable_ = val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool auto_disable_ = false;
|
||||||
|
u_int8_t auto_disable_count_ = 3;
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
} // namespace truma_inetbox
|
||||||
|
} // namespace esphome
|
||||||
34
components/truma_inetbox/time/__init__.py
Normal file
34
components/truma_inetbox/time/__init__.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from esphome.components import time
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
)
|
||||||
|
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
|
||||||
|
|
||||||
|
DEPENDENCIES = ["truma_inetbox"]
|
||||||
|
CODEOWNERS = ["@Fabian-Schmidt"]
|
||||||
|
|
||||||
|
CONF_AUTO_DISABLE = "auto_disable"
|
||||||
|
|
||||||
|
TrumaTime = truma_inetbox_ns.class_(
|
||||||
|
"TrumaTime", time.RealTimeClock, cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TrumaTime),
|
||||||
|
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
|
||||||
|
cv.Optional(CONF_AUTO_DISABLE, default=True): cv.boolean,
|
||||||
|
}
|
||||||
|
).extend(cv.polling_component_schema("never"))
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await time.register_time(var, config)
|
||||||
|
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
|
||||||
|
|
||||||
|
if config[CONF_AUTO_DISABLE]:
|
||||||
|
cg.add(var.set_auto_disable(config[CONF_AUTO_DISABLE]))
|
||||||
335
components/uart/__init__.py
Normal file
335
components/uart/__init__.py
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
from esphome.yaml_util import make_data_base
|
||||||
|
from esphome import pins, automation
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BAUD_RATE,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_NUMBER,
|
||||||
|
CONF_RX_PIN,
|
||||||
|
CONF_TX_PIN,
|
||||||
|
CONF_UART_ID,
|
||||||
|
CONF_DATA,
|
||||||
|
CONF_RX_BUFFER_SIZE,
|
||||||
|
CONF_INVERTED,
|
||||||
|
CONF_INVERT,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
|
CONF_SEQUENCE,
|
||||||
|
CONF_TIMEOUT,
|
||||||
|
CONF_DEBUG,
|
||||||
|
CONF_DIRECTION,
|
||||||
|
CONF_AFTER,
|
||||||
|
CONF_BYTES,
|
||||||
|
CONF_DELIMITER,
|
||||||
|
CONF_DUMMY_RECEIVER,
|
||||||
|
CONF_DUMMY_RECEIVER_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
uart_ns = cg.esphome_ns.namespace("uart")
|
||||||
|
UARTComponent = uart_ns.class_("UARTComponent")
|
||||||
|
|
||||||
|
IDFUARTComponent = uart_ns.class_(
|
||||||
|
"truma_IDFUARTComponent", UARTComponent, cg.Component)
|
||||||
|
ESP32ArduinoUARTComponent = uart_ns.class_(
|
||||||
|
"truma_ESP32ArduinoUARTComponent", UARTComponent, cg.Component
|
||||||
|
)
|
||||||
|
ESP8266UartComponent = uart_ns.class_(
|
||||||
|
"ESP8266UartComponent", UARTComponent, cg.Component
|
||||||
|
)
|
||||||
|
RP2040UartComponent = uart_ns.class_(
|
||||||
|
"truma_RP2040UartComponent", UARTComponent, cg.Component)
|
||||||
|
|
||||||
|
UARTDevice = uart_ns.class_("UARTDevice")
|
||||||
|
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
|
||||||
|
UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action)
|
||||||
|
UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component)
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_raw_data(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.encode("utf-8")
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
if isinstance(value, list):
|
||||||
|
return cv.Schema([cv.hex_uint8_t])(value)
|
||||||
|
raise cv.Invalid(
|
||||||
|
"data must either be a string wrapped in quotes or a list of bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rx_pin(value):
|
||||||
|
value = pins.internal_gpio_input_pin_schema(value)
|
||||||
|
if CORE.is_esp8266 and value[CONF_NUMBER] >= 16:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Pins GPIO16 and GPIO17 cannot be used as RX pins on ESP8266.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_invert_esp32(config):
|
||||||
|
if (
|
||||||
|
CORE.is_esp32
|
||||||
|
and CONF_TX_PIN in config
|
||||||
|
and CONF_RX_PIN in config
|
||||||
|
and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED]
|
||||||
|
):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Different invert values for TX and RX pin are not (yet) supported for ESP32."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _uart_declare_type(value):
|
||||||
|
if CORE.is_esp8266:
|
||||||
|
return cv.declare_id(ESP8266UartComponent)(value)
|
||||||
|
if CORE.is_esp32:
|
||||||
|
if CORE.using_arduino:
|
||||||
|
return cv.declare_id(ESP32ArduinoUARTComponent)(value)
|
||||||
|
if CORE.using_esp_idf:
|
||||||
|
return cv.declare_id(IDFUARTComponent)(value)
|
||||||
|
if CORE.is_rp2040:
|
||||||
|
return cv.declare_id(RP2040UartComponent)(value)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
UARTParityOptions = uart_ns.enum("UARTParityOptions")
|
||||||
|
UART_PARITY_OPTIONS = {
|
||||||
|
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
|
||||||
|
"EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN,
|
||||||
|
"ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONF_STOP_BITS = "stop_bits"
|
||||||
|
CONF_DATA_BITS = "data_bits"
|
||||||
|
CONF_PARITY = "parity"
|
||||||
|
|
||||||
|
UARTDirection = uart_ns.enum("UARTDirection")
|
||||||
|
UART_DIRECTIONS = {
|
||||||
|
"RX": UARTDirection.UART_DIRECTION_RX,
|
||||||
|
"TX": UARTDirection.UART_DIRECTION_TX,
|
||||||
|
"BOTH": UARTDirection.UART_DIRECTION_BOTH,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The reason for having CONF_BYTES at 150 by default:
|
||||||
|
#
|
||||||
|
# The log message buffer size is 512 bytes by default. About 35 bytes are
|
||||||
|
# used for the log prefix. That leaves us with 477 bytes for logging data.
|
||||||
|
# The default log output is hex, which uses 3 characters per represented
|
||||||
|
# byte (2 hex chars + 1 separator). That means that 477 / 3 = 159 bytes
|
||||||
|
# can be represented in a single log line. Using 150, because people love
|
||||||
|
# round numbers.
|
||||||
|
AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"}
|
||||||
|
|
||||||
|
# By default, log in hex format when no specific sequence is provided.
|
||||||
|
DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':');"
|
||||||
|
DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}]
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_empty_debug(value):
|
||||||
|
if value is None:
|
||||||
|
value = {}
|
||||||
|
return DEBUG_SCHEMA(value)
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger),
|
||||||
|
cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum(
|
||||||
|
UART_DIRECTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_AFTER, default=AFTER_DEFAULTS): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(
|
||||||
|
CONF_BYTES, default=AFTER_DEFAULTS[CONF_BYTES]
|
||||||
|
): cv.validate_bytes,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_TIMEOUT, default=AFTER_DEFAULTS[CONF_TIMEOUT]
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Optional(
|
||||||
|
CONF_SEQUENCE, default=DEFAULT_SEQUENCE
|
||||||
|
): automation.validate_automation(),
|
||||||
|
cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean,
|
||||||
|
cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): _uart_declare_type,
|
||||||
|
cv.Required(CONF_BAUD_RATE): cv.int_range(min=1),
|
||||||
|
cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_RX_PIN): validate_rx_pin,
|
||||||
|
cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes,
|
||||||
|
cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
|
||||||
|
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8),
|
||||||
|
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
|
||||||
|
UART_PARITY_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_INVERT): cv.invalid(
|
||||||
|
"This option has been removed. Please instead use invert in the tx/rx pin schemas."
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_DEBUG): maybe_empty_debug,
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN),
|
||||||
|
validate_invert_esp32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def debug_to_code(config, parent):
|
||||||
|
trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], parent)
|
||||||
|
await cg.register_component(trigger, config)
|
||||||
|
for action in config[CONF_SEQUENCE]:
|
||||||
|
await automation.build_automation(
|
||||||
|
trigger,
|
||||||
|
[(UARTDirection, "direction"),
|
||||||
|
(cg.std_vector.template(cg.uint8), "bytes")],
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
cg.add(trigger.set_direction(config[CONF_DIRECTION]))
|
||||||
|
after = config[CONF_AFTER]
|
||||||
|
cg.add(trigger.set_after_bytes(after[CONF_BYTES]))
|
||||||
|
cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT]))
|
||||||
|
if CONF_DELIMITER in after:
|
||||||
|
data = after[CONF_DELIMITER]
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = list(data)
|
||||||
|
for byte in after[CONF_DELIMITER]:
|
||||||
|
cg.add(trigger.add_delimiter_byte(byte))
|
||||||
|
if config[CONF_DUMMY_RECEIVER]:
|
||||||
|
dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent)
|
||||||
|
await cg.register_component(dummy, {})
|
||||||
|
cg.add_define("USE_UART_DEBUGGER")
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
cg.add_global(uart_ns.using)
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
cg.add(var.set_baud_rate(config[CONF_BAUD_RATE]))
|
||||||
|
|
||||||
|
if CONF_TX_PIN in config:
|
||||||
|
tx_pin = await cg.gpio_pin_expression(config[CONF_TX_PIN])
|
||||||
|
cg.add(var.set_tx_pin(tx_pin))
|
||||||
|
if CONF_RX_PIN in config:
|
||||||
|
rx_pin = await cg.gpio_pin_expression(config[CONF_RX_PIN])
|
||||||
|
cg.add(var.set_rx_pin(rx_pin))
|
||||||
|
cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE]))
|
||||||
|
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
|
||||||
|
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
|
||||||
|
cg.add(var.set_parity(config[CONF_PARITY]))
|
||||||
|
|
||||||
|
if CONF_DEBUG in config:
|
||||||
|
await debug_to_code(config[CONF_DEBUG], var)
|
||||||
|
|
||||||
|
|
||||||
|
# A schema to use for all UART devices, all UART integrations must extend this!
|
||||||
|
UART_DEVICE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_UART_ID): cv.use_id(UARTComponent),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
KEY_UART_DEVICES = "uart_devices"
|
||||||
|
|
||||||
|
|
||||||
|
def final_validate_device_schema(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
baud_rate: Optional[int] = None,
|
||||||
|
require_tx: bool = False,
|
||||||
|
require_rx: bool = False,
|
||||||
|
):
|
||||||
|
def validate_baud_rate(value):
|
||||||
|
if value != baud_rate:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Component {name} required baud rate {baud_rate} for the uart bus"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_pin(opt, device):
|
||||||
|
def validator(value):
|
||||||
|
if opt in device:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"The uart {opt} is used both by {name} and {device[opt]}, "
|
||||||
|
f"but can only be used by one. Please create a new uart bus for {name}."
|
||||||
|
)
|
||||||
|
device[opt] = name
|
||||||
|
return value
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
def validate_hub(hub_config):
|
||||||
|
hub_schema = {}
|
||||||
|
uart_id = hub_config[CONF_ID]
|
||||||
|
devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {})
|
||||||
|
device = devices.setdefault(uart_id, {})
|
||||||
|
|
||||||
|
if require_tx:
|
||||||
|
hub_schema[
|
||||||
|
cv.Required(
|
||||||
|
CONF_TX_PIN,
|
||||||
|
msg=f"Component {name} requires this uart bus to declare a tx_pin",
|
||||||
|
)
|
||||||
|
] = validate_pin(CONF_TX_PIN, device)
|
||||||
|
if require_rx:
|
||||||
|
hub_schema[
|
||||||
|
cv.Required(
|
||||||
|
CONF_RX_PIN,
|
||||||
|
msg=f"Component {name} requires this uart bus to declare a rx_pin",
|
||||||
|
)
|
||||||
|
] = validate_pin(CONF_RX_PIN, device)
|
||||||
|
if baud_rate is not None:
|
||||||
|
hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate
|
||||||
|
return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
|
||||||
|
|
||||||
|
return cv.Schema(
|
||||||
|
{cv.Required(CONF_UART_ID) : fv.id_declaration_match_schema(validate_hub)},
|
||||||
|
extra=cv.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def register_uart_device(var, config):
|
||||||
|
"""Register a UART device, setting up all the internal values.
|
||||||
|
|
||||||
|
This is a coroutine, you need to await it with a 'yield' expression!
|
||||||
|
"""
|
||||||
|
parent = await cg.get_variable(config[CONF_UART_ID])
|
||||||
|
cg.add(var.set_uart_parent(parent))
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"uart.write",
|
||||||
|
UARTWriteAction,
|
||||||
|
cv.maybe_simple_value(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(UARTComponent),
|
||||||
|
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||||
|
},
|
||||||
|
key=CONF_DATA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def uart_write_to_code(config, action_id, template_arg, args):
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_parented(var, config[CONF_ID])
|
||||||
|
data = config[CONF_DATA]
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = list(data)
|
||||||
|
|
||||||
|
if cg.is_template(data):
|
||||||
|
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||||
|
cg.add(var.set_data_template(templ))
|
||||||
|
else:
|
||||||
|
cg.add(var.set_data_static(data))
|
||||||
|
return var
|
||||||
38
components/uart/automation.h
Normal file
38
components/uart/automation.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "uart.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
template<typename... Ts> class UARTWriteAction : public Action<Ts...>, public Parented<UARTComponent> {
|
||||||
|
public:
|
||||||
|
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||||
|
this->data_func_ = func;
|
||||||
|
this->static_ = false;
|
||||||
|
}
|
||||||
|
void set_data_static(const std::vector<uint8_t> &data) {
|
||||||
|
this->data_static_ = data;
|
||||||
|
this->static_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
if (this->static_) {
|
||||||
|
this->parent_->write_array(this->data_static_);
|
||||||
|
} else {
|
||||||
|
auto val = this->data_func_(x...);
|
||||||
|
this->parent_->write_array(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool static_{false};
|
||||||
|
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||||
|
std::vector<uint8_t> data_static_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
37
components/uart/switch/__init__.py
Normal file
37
components/uart/switch/__init__.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import switch, uart
|
||||||
|
from esphome.const import CONF_DATA, CONF_SEND_EVERY
|
||||||
|
from esphome.core import HexInt
|
||||||
|
from .. import uart_ns, validate_raw_data
|
||||||
|
|
||||||
|
DEPENDENCIES = ["uart"]
|
||||||
|
|
||||||
|
UARTSwitch = uart_ns.class_("UARTSwitch", switch.Switch, uart.UARTDevice, cg.Component)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
switch.switch_schema(UARTSwitch, block_inverted=True)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_DATA): validate_raw_data,
|
||||||
|
cv.Optional(CONF_SEND_EVERY): cv.positive_time_period_milliseconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await switch.new_switch(config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await uart.register_uart_device(var, config)
|
||||||
|
|
||||||
|
data = config[CONF_DATA]
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = [HexInt(x) for x in data]
|
||||||
|
cg.add(var.set_data(data))
|
||||||
|
|
||||||
|
if CONF_SEND_EVERY in config:
|
||||||
|
cg.add(var.set_send_every(config[CONF_SEND_EVERY]))
|
||||||
47
components/uart/switch/uart_switch.cpp
Normal file
47
components/uart/switch/uart_switch.cpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#include "uart_switch.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart.switch";
|
||||||
|
|
||||||
|
void UARTSwitch::loop() {
|
||||||
|
if (this->state && this->send_every_) {
|
||||||
|
const uint32_t now = millis();
|
||||||
|
if (now - this->last_transmission_ > this->send_every_) {
|
||||||
|
this->write_command_();
|
||||||
|
this->last_transmission_ = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTSwitch::write_command_() {
|
||||||
|
ESP_LOGD(TAG, "'%s': Sending data...", this->get_name().c_str());
|
||||||
|
this->write_array(this->data_.data(), this->data_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTSwitch::write_state(bool state) {
|
||||||
|
if (!state) {
|
||||||
|
this->publish_state(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->publish_state(true);
|
||||||
|
this->write_command_();
|
||||||
|
|
||||||
|
if (this->send_every_ == 0) {
|
||||||
|
this->publish_state(false);
|
||||||
|
} else {
|
||||||
|
this->last_transmission_ = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void UARTSwitch::dump_config() {
|
||||||
|
LOG_SWITCH("", "UART Switch", this);
|
||||||
|
if (this->send_every_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Send Every: %u", this->send_every_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
30
components/uart/switch/uart_switch.h
Normal file
30
components/uart/switch/uart_switch.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#include "esphome/components/switch/switch.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class UARTSwitch : public switch_::Switch, public UARTDevice, public Component {
|
||||||
|
public:
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
void set_data(const std::vector<uint8_t> &data) { data_ = data; }
|
||||||
|
void set_send_every(uint32_t send_every) { this->send_every_ = send_every; }
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void write_command_();
|
||||||
|
void write_state(bool state) override;
|
||||||
|
std::vector<uint8_t> data_;
|
||||||
|
uint32_t send_every_;
|
||||||
|
uint32_t last_transmission_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
20
components/uart/truma_uart_component_esp32_arduino.h
Normal file
20
components/uart/truma_uart_component_esp32_arduino.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
|
||||||
|
#include "uart_component_esp32_arduino.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class truma_ESP32ArduinoUARTComponent : public ESP32ArduinoUARTComponent {
|
||||||
|
public:
|
||||||
|
bool is_hw_serial() { return true; }
|
||||||
|
HardwareSerial *get_hw_serial() { return this->hw_serial_; }
|
||||||
|
uint8_t get_hw_serial_number() { return this->number_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
19
components/uart/truma_uart_component_esp_idf.h
Normal file
19
components/uart/truma_uart_component_esp_idf.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
|
||||||
|
#include "uart_component_esp_idf.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class truma_IDFUARTComponent : public IDFUARTComponent {
|
||||||
|
public:
|
||||||
|
bool is_hw_serial() { return true; }
|
||||||
|
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP_IDF
|
||||||
19
components/uart/truma_uart_component_rp2040.h
Normal file
19
components/uart/truma_uart_component_rp2040.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
|
||||||
|
#include "uart_component_rp2040.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class truma_RP2040UartComponent : public RP2040UartComponent {
|
||||||
|
public:
|
||||||
|
bool is_hw_serial() { return this->hw_serial_; }
|
||||||
|
HardwareSerial *get_hw_serial() { return this->serial_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RP2040
|
||||||
46
components/uart/uart.cpp
Normal file
46
components/uart/uart.cpp
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#include "uart.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart";
|
||||||
|
|
||||||
|
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
|
||||||
|
uint8_t data_bits) {
|
||||||
|
if (this->parent_->get_baud_rate() != baud_rate) {
|
||||||
|
ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate,
|
||||||
|
this->parent_->get_baud_rate());
|
||||||
|
}
|
||||||
|
if (this->parent_->get_stop_bits() != stop_bits) {
|
||||||
|
ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits,
|
||||||
|
this->parent_->get_stop_bits());
|
||||||
|
}
|
||||||
|
if (this->parent_->get_data_bits() != data_bits) {
|
||||||
|
ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits,
|
||||||
|
this->parent_->get_data_bits());
|
||||||
|
}
|
||||||
|
if (this->parent_->get_parity() != parity) {
|
||||||
|
ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!",
|
||||||
|
LOG_STR_ARG(parity_to_str(parity)), LOG_STR_ARG(parity_to_str(this->parent_->get_parity())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogString *parity_to_str(UARTParityOptions parity) {
|
||||||
|
switch (parity) {
|
||||||
|
case UART_CONFIG_PARITY_NONE:
|
||||||
|
return LOG_STR("NONE");
|
||||||
|
case UART_CONFIG_PARITY_EVEN:
|
||||||
|
return LOG_STR("EVEN");
|
||||||
|
case UART_CONFIG_PARITY_ODD:
|
||||||
|
return LOG_STR("ODD");
|
||||||
|
default:
|
||||||
|
return LOG_STR("UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
72
components/uart/uart.h
Normal file
72
components/uart/uart.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class UARTDevice {
|
||||||
|
public:
|
||||||
|
UARTDevice() = default;
|
||||||
|
UARTDevice(UARTComponent *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
void set_uart_parent(UARTComponent *parent) { this->parent_ = parent; }
|
||||||
|
|
||||||
|
void write_byte(uint8_t data) { this->parent_->write_byte(data); }
|
||||||
|
|
||||||
|
void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); }
|
||||||
|
void write_array(const std::vector<uint8_t> &data) { this->parent_->write_array(data); }
|
||||||
|
template<size_t N> void write_array(const std::array<uint8_t, N> &data) {
|
||||||
|
this->parent_->write_array(data.data(), data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_str(const char *str) { this->parent_->write_str(str); }
|
||||||
|
|
||||||
|
bool read_byte(uint8_t *data) { return this->parent_->read_byte(data); }
|
||||||
|
bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); }
|
||||||
|
|
||||||
|
bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); }
|
||||||
|
template<size_t N> optional<std::array<uint8_t, N>> read_array() { // NOLINT
|
||||||
|
std::array<uint8_t, N> res;
|
||||||
|
if (!this->read_array(res.data(), N)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
int available() { return this->parent_->available(); }
|
||||||
|
|
||||||
|
void flush() { return this->parent_->flush(); }
|
||||||
|
|
||||||
|
// Compat APIs
|
||||||
|
int read() {
|
||||||
|
uint8_t data;
|
||||||
|
if (!this->read_byte(&data))
|
||||||
|
return -1;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
size_t write(uint8_t data) {
|
||||||
|
this->write_byte(data);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int peek() {
|
||||||
|
uint8_t data;
|
||||||
|
if (!this->peek_byte(&data))
|
||||||
|
return -1;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
|
||||||
|
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
|
||||||
|
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UARTComponent *parent_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
24
components/uart/uart_component.cpp
Normal file
24
components/uart/uart_component.cpp
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart";
|
||||||
|
|
||||||
|
bool UARTComponent::check_read_timeout_(size_t len) {
|
||||||
|
if (this->available() >= int(len))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
uint32_t start_time = millis();
|
||||||
|
while (this->available() < int(len)) {
|
||||||
|
if (millis() - start_time > 100) {
|
||||||
|
ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
89
components/uart/uart_component.h
Normal file
89
components/uart/uart_component.h
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
enum UARTParityOptions {
|
||||||
|
UART_CONFIG_PARITY_NONE,
|
||||||
|
UART_CONFIG_PARITY_EVEN,
|
||||||
|
UART_CONFIG_PARITY_ODD,
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
enum UARTDirection {
|
||||||
|
UART_DIRECTION_RX,
|
||||||
|
UART_DIRECTION_TX,
|
||||||
|
UART_DIRECTION_BOTH,
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const LogString *parity_to_str(UARTParityOptions parity);
|
||||||
|
|
||||||
|
class UARTComponent {
|
||||||
|
public:
|
||||||
|
void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); }
|
||||||
|
void write_byte(uint8_t data) { this->write_array(&data, 1); };
|
||||||
|
void write_str(const char *str) {
|
||||||
|
const auto *data = reinterpret_cast<const uint8_t *>(str);
|
||||||
|
this->write_array(data, strlen(str));
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual void write_array(const uint8_t *data, size_t len) = 0;
|
||||||
|
|
||||||
|
bool read_byte(uint8_t *data) { return this->read_array(data, 1); };
|
||||||
|
virtual bool peek_byte(uint8_t *data) = 0;
|
||||||
|
virtual bool read_array(uint8_t *data, size_t len) = 0;
|
||||||
|
|
||||||
|
/// Return available number of bytes.
|
||||||
|
virtual int available() = 0;
|
||||||
|
/// Block until all bytes have been written to the UART bus.
|
||||||
|
virtual void flush() = 0;
|
||||||
|
|
||||||
|
void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; }
|
||||||
|
void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; }
|
||||||
|
void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; }
|
||||||
|
size_t get_rx_buffer_size() { return this->rx_buffer_size_; }
|
||||||
|
|
||||||
|
void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; }
|
||||||
|
uint8_t get_stop_bits() const { return this->stop_bits_; }
|
||||||
|
void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; }
|
||||||
|
uint8_t get_data_bits() const { return this->data_bits_; }
|
||||||
|
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
|
||||||
|
UARTParityOptions get_parity() const { return this->parity_; }
|
||||||
|
void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; }
|
||||||
|
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||||
|
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) {
|
||||||
|
this->debug_callback_.add(std::move(callback));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void check_logger_conflict() = 0;
|
||||||
|
bool check_read_timeout_(size_t len = 1);
|
||||||
|
|
||||||
|
InternalGPIOPin *tx_pin_;
|
||||||
|
InternalGPIOPin *rx_pin_;
|
||||||
|
size_t rx_buffer_size_;
|
||||||
|
uint32_t baud_rate_;
|
||||||
|
uint8_t stop_bits_;
|
||||||
|
uint8_t data_bits_;
|
||||||
|
UARTParityOptions parity_;
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
170
components/uart/uart_component_esp32_arduino.cpp
Normal file
170
components/uart/uart_component_esp32_arduino.cpp
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "uart_component_esp32_arduino.h"
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
#include "esphome/components/logger/logger.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
static const char *const TAG = "uart.arduino_esp32";
|
||||||
|
|
||||||
|
static const uint32_t UART_PARITY_EVEN = 0 << 0;
|
||||||
|
static const uint32_t UART_PARITY_ODD = 1 << 0;
|
||||||
|
static const uint32_t UART_PARITY_ENABLE = 1 << 1;
|
||||||
|
static const uint32_t UART_NB_BIT_5 = 0 << 2;
|
||||||
|
static const uint32_t UART_NB_BIT_6 = 1 << 2;
|
||||||
|
static const uint32_t UART_NB_BIT_7 = 2 << 2;
|
||||||
|
static const uint32_t UART_NB_BIT_8 = 3 << 2;
|
||||||
|
static const uint32_t UART_NB_STOP_BIT_1 = 1 << 4;
|
||||||
|
static const uint32_t UART_NB_STOP_BIT_2 = 3 << 4;
|
||||||
|
static const uint32_t UART_TICK_APB_CLOCK = 1 << 27;
|
||||||
|
|
||||||
|
uint32_t ESP32ArduinoUARTComponent::get_config() {
|
||||||
|
uint32_t config = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All bits numbers below come from
|
||||||
|
* framework-arduinoespressif32/cores/esp32/esp32-hal-uart.h
|
||||||
|
* And more specifically conf0 union in uart_dev_t.
|
||||||
|
*
|
||||||
|
* Below is bit used from conf0 union.
|
||||||
|
* <name>:<bits position> <values>
|
||||||
|
* parity:0 0:even 1:odd
|
||||||
|
* parity_en:1 Set this bit to enable uart parity check.
|
||||||
|
* bit_num:2-4 0:5bits 1:6bits 2:7bits 3:8bits
|
||||||
|
* stop_bit_num:4-6 stop bit. 1:1bit 2:1.5bits 3:2bits
|
||||||
|
* tick_ref_always_on:27 select the clock.1:apb clock:ref_tick
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||||
|
config |= UART_PARITY_EVEN | UART_PARITY_ENABLE;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
|
||||||
|
config |= UART_PARITY_ODD | UART_PARITY_ENABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this->data_bits_) {
|
||||||
|
case 5:
|
||||||
|
config |= UART_NB_BIT_5;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
config |= UART_NB_BIT_6;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
config |= UART_NB_BIT_7;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
config |= UART_NB_BIT_8;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->stop_bits_ == 1) {
|
||||||
|
config |= UART_NB_STOP_BIT_1;
|
||||||
|
} else {
|
||||||
|
config |= UART_NB_STOP_BIT_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
config |= UART_TICK_APB_CLOCK;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32ArduinoUARTComponent::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up UART...");
|
||||||
|
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
||||||
|
// preconfigured by the platform. For example if RX disabled but TX pin
|
||||||
|
// is 1 we still want to use Serial.
|
||||||
|
bool is_default_tx, is_default_rx;
|
||||||
|
#ifdef CONFIG_IDF_TARGET_ESP32C3
|
||||||
|
is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 21;
|
||||||
|
is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 20;
|
||||||
|
#else
|
||||||
|
is_default_tx = tx_pin_ == nullptr || tx_pin_->get_pin() == 1;
|
||||||
|
is_default_rx = rx_pin_ == nullptr || rx_pin_->get_pin() == 3;
|
||||||
|
#endif
|
||||||
|
if (is_default_tx && is_default_rx) {
|
||||||
|
this->hw_serial_ = &Serial;
|
||||||
|
} else {
|
||||||
|
static uint8_t next_uart_num = 1;
|
||||||
|
this->number_ = next_uart_num;
|
||||||
|
this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
}
|
||||||
|
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||||
|
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||||
|
bool invert = false;
|
||||||
|
if (tx_pin_ != nullptr && tx_pin_->is_inverted())
|
||||||
|
invert = true;
|
||||||
|
if (rx_pin_ != nullptr && rx_pin_->is_inverted())
|
||||||
|
invert = true;
|
||||||
|
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||||
|
this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32ArduinoUARTComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "UART Bus %d:", this->number_);
|
||||||
|
LOG_PIN(" TX Pin: ", tx_pin_);
|
||||||
|
LOG_PIN(" RX Pin: ", rx_pin_);
|
||||||
|
if (this->rx_pin_ != nullptr) {
|
||||||
|
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
|
||||||
|
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
|
||||||
|
this->check_logger_conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||||
|
this->hw_serial_->write(data, len);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) {
|
||||||
|
if (!this->check_read_timeout_())
|
||||||
|
return false;
|
||||||
|
*data = this->hw_serial_->peek();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||||
|
if (!this->check_read_timeout_(len))
|
||||||
|
return false;
|
||||||
|
this->hw_serial_->readBytes(data, len);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); }
|
||||||
|
void ESP32ArduinoUARTComponent::flush() {
|
||||||
|
ESP_LOGVV(TAG, " Flushing...");
|
||||||
|
this->hw_serial_->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32ArduinoUARTComponent::check_logger_conflict() {
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->hw_serial_ == logger::global_logger->get_hw_serial()) {
|
||||||
|
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
|
||||||
|
"disable logging over the serial port by setting logger->baud_rate to 0.");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
40
components/uart/uart_component_esp32_arduino.h
Normal file
40
components/uart/uart_component_esp32_arduino.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class ESP32ArduinoUARTComponent : public UARTComponent, public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||||
|
|
||||||
|
void write_array(const uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
bool peek_byte(uint8_t *data) override;
|
||||||
|
bool read_array(uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
int available() override;
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
uint32_t get_config();
|
||||||
|
protected:
|
||||||
|
void check_logger_conflict() override;
|
||||||
|
|
||||||
|
HardwareSerial *hw_serial_{nullptr};
|
||||||
|
uint8_t number_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||||
304
components/uart/uart_component_esp8266.cpp
Normal file
304
components/uart/uart_component_esp8266.cpp
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
#ifdef USE_ESP8266
|
||||||
|
#include "uart_component_esp8266.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
#include "esphome/components/logger/logger.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart.arduino_esp8266";
|
||||||
|
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
uint32_t ESP8266UartComponent::get_config() {
|
||||||
|
uint32_t config = 0;
|
||||||
|
|
||||||
|
if (this->parity_ == UART_CONFIG_PARITY_NONE) {
|
||||||
|
config |= UART_PARITY_NONE;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||||
|
config |= UART_PARITY_EVEN;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
|
||||||
|
config |= UART_PARITY_ODD;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this->data_bits_) {
|
||||||
|
case 5:
|
||||||
|
config |= UART_NB_BIT_5;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
config |= UART_NB_BIT_6;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
config |= UART_NB_BIT_7;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
config |= UART_NB_BIT_8;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->stop_bits_ == 1) {
|
||||||
|
config |= UART_NB_STOP_BIT_1;
|
||||||
|
} else {
|
||||||
|
config |= UART_NB_STOP_BIT_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
|
||||||
|
config |= BIT(22);
|
||||||
|
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
|
||||||
|
config |= BIT(19);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP8266UartComponent::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up UART bus...");
|
||||||
|
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
||||||
|
// preconfigured by the platform. For example if RX disabled but TX pin
|
||||||
|
// is 1 we still want to use Serial.
|
||||||
|
SerialConfig config = static_cast<SerialConfig>(get_config());
|
||||||
|
|
||||||
|
if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) &&
|
||||||
|
(rx_pin_ == nullptr || rx_pin_->get_pin() == 3)
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
// we will use UART0 if logger isn't using it in swapped mode
|
||||||
|
&& (logger::global_logger->get_hw_serial() == nullptr ||
|
||||||
|
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP)
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
|
this->hw_serial_ = &Serial;
|
||||||
|
this->hw_serial_->begin(this->baud_rate_, config);
|
||||||
|
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||||
|
ESP8266UartComponent::serial0_in_use = true;
|
||||||
|
} else if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 15) &&
|
||||||
|
(rx_pin_ == nullptr || rx_pin_->get_pin() == 13)
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
// we will use UART0 swapped if logger isn't using it in regular mode
|
||||||
|
&& (logger::global_logger->get_hw_serial() == nullptr ||
|
||||||
|
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0)
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
|
this->hw_serial_ = &Serial;
|
||||||
|
this->hw_serial_->begin(this->baud_rate_, config);
|
||||||
|
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||||
|
this->hw_serial_->swap();
|
||||||
|
ESP8266UartComponent::serial0_in_use = true;
|
||||||
|
} else if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) {
|
||||||
|
this->hw_serial_ = &Serial1;
|
||||||
|
this->hw_serial_->begin(this->baud_rate_, config);
|
||||||
|
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||||
|
} else {
|
||||||
|
this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT
|
||||||
|
this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_,
|
||||||
|
this->rx_buffer_size_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP8266UartComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "UART Bus:");
|
||||||
|
LOG_PIN(" TX Pin: ", tx_pin_);
|
||||||
|
LOG_PIN(" RX Pin: ", rx_pin_);
|
||||||
|
if (this->rx_pin_ != nullptr) {
|
||||||
|
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
|
||||||
|
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Using hardware serial interface.");
|
||||||
|
} else {
|
||||||
|
ESP_LOGCONFIG(TAG, " Using software serial");
|
||||||
|
}
|
||||||
|
this->check_logger_conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP8266UartComponent::check_logger_conflict() {
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->hw_serial_ == logger::global_logger->get_hw_serial()) {
|
||||||
|
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
|
||||||
|
"disable logging over the serial port by setting logger->baud_rate to 0.");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) {
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
this->hw_serial_->write(data, len);
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < len; i++)
|
||||||
|
this->sw_serial_->write_byte(data[i]);
|
||||||
|
}
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
bool ESP8266UartComponent::peek_byte(uint8_t *data) {
|
||||||
|
if (!this->check_read_timeout_())
|
||||||
|
return false;
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
*data = this->hw_serial_->peek();
|
||||||
|
} else {
|
||||||
|
*data = this->sw_serial_->peek_byte();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
|
||||||
|
if (!this->check_read_timeout_(len))
|
||||||
|
return false;
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
this->hw_serial_->readBytes(data, len);
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < len; i++)
|
||||||
|
data[i] = this->sw_serial_->read_byte();
|
||||||
|
}
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int ESP8266UartComponent::available() {
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
return this->hw_serial_->available();
|
||||||
|
} else {
|
||||||
|
return this->sw_serial_->available();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void ESP8266UartComponent::flush() {
|
||||||
|
ESP_LOGVV(TAG, " Flushing...");
|
||||||
|
if (this->hw_serial_ != nullptr) {
|
||||||
|
this->hw_serial_->flush();
|
||||||
|
} else {
|
||||||
|
this->sw_serial_->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate,
|
||||||
|
uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity,
|
||||||
|
size_t rx_buffer_size) {
|
||||||
|
this->bit_time_ = F_CPU / baud_rate;
|
||||||
|
this->rx_buffer_size_ = rx_buffer_size;
|
||||||
|
this->stop_bits_ = stop_bits;
|
||||||
|
this->data_bits_ = data_bits;
|
||||||
|
this->parity_ = parity;
|
||||||
|
if (tx_pin != nullptr) {
|
||||||
|
gpio_tx_pin_ = tx_pin;
|
||||||
|
gpio_tx_pin_->setup();
|
||||||
|
tx_pin_ = gpio_tx_pin_->to_isr();
|
||||||
|
tx_pin_.digital_write(true);
|
||||||
|
}
|
||||||
|
if (rx_pin != nullptr) {
|
||||||
|
gpio_rx_pin_ = rx_pin;
|
||||||
|
gpio_rx_pin_->setup();
|
||||||
|
rx_pin_ = gpio_rx_pin_->to_isr();
|
||||||
|
rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT
|
||||||
|
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
|
||||||
|
uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500;
|
||||||
|
const uint32_t start = arch_get_cpu_cycle_count();
|
||||||
|
uint8_t rec = 0;
|
||||||
|
// Manually unroll the loop
|
||||||
|
for (int i = 0; i < arg->data_bits_; i++)
|
||||||
|
rec |= arg->read_bit_(&wait, start) << i;
|
||||||
|
|
||||||
|
/* If parity is enabled, just read it and ignore it. */
|
||||||
|
/* TODO: Should we check parity? Or is it too slow for nothing added..*/
|
||||||
|
if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD)
|
||||||
|
arg->read_bit_(&wait, start);
|
||||||
|
|
||||||
|
// Stop bit
|
||||||
|
arg->wait_(&wait, start);
|
||||||
|
if (arg->stop_bits_ == 2)
|
||||||
|
arg->wait_(&wait, start);
|
||||||
|
|
||||||
|
arg->rx_buffer_[arg->rx_in_pos_] = rec;
|
||||||
|
arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
|
||||||
|
// Clear RX pin so that the interrupt doesn't re-trigger right away again.
|
||||||
|
arg->rx_pin_.clear_interrupt();
|
||||||
|
}
|
||||||
|
void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
|
||||||
|
if (this->gpio_tx_pin_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "UART doesn't have TX pins set!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool parity_bit = false;
|
||||||
|
bool need_parity_bit = true;
|
||||||
|
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||||
|
parity_bit = false;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
|
||||||
|
parity_bit = true;
|
||||||
|
} else {
|
||||||
|
need_parity_bit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
InterruptLock lock;
|
||||||
|
uint32_t wait = this->bit_time_;
|
||||||
|
const uint32_t start = arch_get_cpu_cycle_count();
|
||||||
|
// Start bit
|
||||||
|
this->write_bit_(false, &wait, start);
|
||||||
|
for (int i = 0; i < this->data_bits_; i++) {
|
||||||
|
bool bit = data & (1 << i);
|
||||||
|
this->write_bit_(bit, &wait, start);
|
||||||
|
if (need_parity_bit)
|
||||||
|
parity_bit ^= bit;
|
||||||
|
}
|
||||||
|
if (need_parity_bit)
|
||||||
|
this->write_bit_(parity_bit, &wait, start);
|
||||||
|
// Stop bit
|
||||||
|
this->write_bit_(true, &wait, start);
|
||||||
|
if (this->stop_bits_ == 2)
|
||||||
|
this->wait_(&wait, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
|
||||||
|
while (arch_get_cpu_cycle_count() - start < *wait)
|
||||||
|
;
|
||||||
|
*wait += this->bit_time_;
|
||||||
|
}
|
||||||
|
bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
|
||||||
|
this->wait_(wait, start);
|
||||||
|
return this->rx_pin_.digital_read();
|
||||||
|
}
|
||||||
|
void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) {
|
||||||
|
this->tx_pin_.digital_write(bit);
|
||||||
|
this->wait_(wait, start);
|
||||||
|
}
|
||||||
|
uint8_t ESP8266SoftwareSerial::read_byte() {
|
||||||
|
if (this->rx_in_pos_ == this->rx_out_pos_)
|
||||||
|
return 0;
|
||||||
|
uint8_t data = this->rx_buffer_[this->rx_out_pos_];
|
||||||
|
this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
uint8_t ESP8266SoftwareSerial::peek_byte() {
|
||||||
|
if (this->rx_in_pos_ == this->rx_out_pos_)
|
||||||
|
return 0;
|
||||||
|
return this->rx_buffer_[this->rx_out_pos_];
|
||||||
|
}
|
||||||
|
void ESP8266SoftwareSerial::flush() {
|
||||||
|
// Flush is a NO-OP with software serial, all bytes are written immediately.
|
||||||
|
}
|
||||||
|
int ESP8266SoftwareSerial::available() {
|
||||||
|
int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_);
|
||||||
|
if (avail < 0)
|
||||||
|
return avail + this->rx_buffer_size_;
|
||||||
|
return avail;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // USE_ESP8266
|
||||||
79
components/uart/uart_component_esp8266.h
Normal file
79
components/uart/uart_component_esp8266.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class ESP8266SoftwareSerial {
|
||||||
|
public:
|
||||||
|
void setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits,
|
||||||
|
uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size);
|
||||||
|
|
||||||
|
uint8_t read_byte();
|
||||||
|
uint8_t peek_byte();
|
||||||
|
|
||||||
|
void flush();
|
||||||
|
|
||||||
|
void write_byte(uint8_t data);
|
||||||
|
|
||||||
|
int available();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static void gpio_intr(ESP8266SoftwareSerial *arg);
|
||||||
|
|
||||||
|
void wait_(uint32_t *wait, const uint32_t &start);
|
||||||
|
bool read_bit_(uint32_t *wait, const uint32_t &start);
|
||||||
|
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start);
|
||||||
|
|
||||||
|
uint32_t bit_time_{0};
|
||||||
|
uint8_t *rx_buffer_{nullptr};
|
||||||
|
size_t rx_buffer_size_;
|
||||||
|
volatile size_t rx_in_pos_{0};
|
||||||
|
size_t rx_out_pos_{0};
|
||||||
|
uint8_t stop_bits_;
|
||||||
|
uint8_t data_bits_;
|
||||||
|
UARTParityOptions parity_;
|
||||||
|
InternalGPIOPin *gpio_tx_pin_{nullptr};
|
||||||
|
ISRInternalGPIOPin tx_pin_;
|
||||||
|
InternalGPIOPin *gpio_rx_pin_{nullptr};
|
||||||
|
ISRInternalGPIOPin rx_pin_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ESP8266UartComponent : public UARTComponent, public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||||
|
|
||||||
|
void write_array(const uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
bool peek_byte(uint8_t *data) override;
|
||||||
|
bool read_array(uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
int available() override;
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
uint32_t get_config();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void check_logger_conflict() override;
|
||||||
|
|
||||||
|
HardwareSerial *hw_serial_{nullptr};
|
||||||
|
ESP8266SoftwareSerial *sw_serial_{nullptr};
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP8266
|
||||||
206
components/uart/uart_component_esp_idf.cpp
Normal file
206
components/uart/uart_component_esp_idf.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
|
||||||
|
#include "uart_component_esp_idf.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
#include "esphome/components/logger/logger.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
static const char *const TAG = "uart.idf";
|
||||||
|
|
||||||
|
uart_config_t IDFUARTComponent::get_config_() {
|
||||||
|
uart_parity_t parity = UART_PARITY_DISABLE;
|
||||||
|
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||||
|
parity = UART_PARITY_EVEN;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
|
||||||
|
parity = UART_PARITY_ODD;
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_word_length_t data_bits;
|
||||||
|
switch (this->data_bits_) {
|
||||||
|
case 5:
|
||||||
|
data_bits = UART_DATA_5_BITS;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
data_bits = UART_DATA_6_BITS;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
data_bits = UART_DATA_7_BITS;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
data_bits = UART_DATA_8_BITS;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data_bits = UART_DATA_BITS_MAX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_config_t uart_config;
|
||||||
|
uart_config.baud_rate = this->baud_rate_;
|
||||||
|
uart_config.data_bits = data_bits;
|
||||||
|
uart_config.parity = parity;
|
||||||
|
uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2;
|
||||||
|
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||||
|
uart_config.source_clk = UART_SCLK_APB;
|
||||||
|
uart_config.rx_flow_ctrl_thresh = 122;
|
||||||
|
|
||||||
|
return uart_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDFUARTComponent::setup() {
|
||||||
|
static uint8_t next_uart_num = 0;
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
if (logger::global_logger->get_uart_num() == next_uart_num)
|
||||||
|
next_uart_num++;
|
||||||
|
#endif
|
||||||
|
if (next_uart_num >= UART_NUM_MAX) {
|
||||||
|
ESP_LOGW(TAG, "Maximum number of UART components created already.");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->uart_num_ = next_uart_num++;
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up UART %u...", this->uart_num_);
|
||||||
|
|
||||||
|
this->lock_ = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
|
||||||
|
uart_config_t uart_config = this->get_config_();
|
||||||
|
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = uart_driver_install(this->uart_num_, this->rx_buffer_size_, 0, 0, nullptr, 0);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||||
|
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||||
|
|
||||||
|
err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t invert = 0;
|
||||||
|
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
|
||||||
|
invert |= UART_SIGNAL_TXD_INV;
|
||||||
|
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
|
||||||
|
invert |= UART_SIGNAL_RXD_INV;
|
||||||
|
|
||||||
|
err = uart_set_line_inverse(this->uart_num_, invert);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDFUARTComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "UART Bus:");
|
||||||
|
ESP_LOGCONFIG(TAG, " Number: %u", this->uart_num_);
|
||||||
|
LOG_PIN(" TX Pin: ", tx_pin_);
|
||||||
|
LOG_PIN(" RX Pin: ", rx_pin_);
|
||||||
|
if (this->rx_pin_ != nullptr) {
|
||||||
|
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
|
||||||
|
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
|
||||||
|
this->check_logger_conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
uart_write_bytes(this->uart_num_, data, len);
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||||
|
if (!this->check_read_timeout_())
|
||||||
|
return false;
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
if (this->has_peek_) {
|
||||||
|
*data = this->peek_byte_;
|
||||||
|
} else {
|
||||||
|
int len = uart_read_bytes(this->uart_num_, data, 1, 20 / portTICK_RATE_MS);
|
||||||
|
if (len == 0) {
|
||||||
|
*data = 0;
|
||||||
|
} else {
|
||||||
|
this->has_peek_ = true;
|
||||||
|
this->peek_byte_ = *data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||||
|
size_t length_to_read = len;
|
||||||
|
if (!this->check_read_timeout_(len))
|
||||||
|
return false;
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
if (this->has_peek_) {
|
||||||
|
length_to_read--;
|
||||||
|
*data = this->peek_byte_;
|
||||||
|
data++;
|
||||||
|
this->has_peek_ = false;
|
||||||
|
}
|
||||||
|
if (length_to_read > 0)
|
||||||
|
uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_RATE_MS);
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IDFUARTComponent::available() {
|
||||||
|
size_t available;
|
||||||
|
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
uart_get_buffered_data_len(this->uart_num_, &available);
|
||||||
|
if (this->has_peek_)
|
||||||
|
available++;
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDFUARTComponent::flush() {
|
||||||
|
ESP_LOGVV(TAG, " Flushing...");
|
||||||
|
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||||
|
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
|
||||||
|
xSemaphoreGive(this->lock_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDFUARTComponent::check_logger_conflict() {}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32
|
||||||
39
components/uart/uart_component_esp_idf.h
Normal file
39
components/uart/uart_component_esp_idf.h
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
|
||||||
|
#include <driver/uart.h>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class IDFUARTComponent : public UARTComponent, public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||||
|
|
||||||
|
void write_array(const uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
bool peek_byte(uint8_t *data) override;
|
||||||
|
bool read_array(uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
int available() override;
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void check_logger_conflict() override;
|
||||||
|
uart_port_t uart_num_;
|
||||||
|
uart_config_t get_config_();
|
||||||
|
SemaphoreHandle_t lock_;
|
||||||
|
|
||||||
|
bool has_peek_{false};
|
||||||
|
uint8_t peek_byte_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP_IDF
|
||||||
184
components/uart/uart_component_rp2040.cpp
Normal file
184
components/uart/uart_component_rp2040.cpp
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#ifdef USE_RP2040
|
||||||
|
#include "uart_component_rp2040.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <hardware/uart.h>
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
#include "esphome/components/logger/logger.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart.arduino_rp2040";
|
||||||
|
|
||||||
|
uint16_t RP2040UartComponent::get_config() {
|
||||||
|
uint16_t config = 0;
|
||||||
|
|
||||||
|
if (this->parity_ == UART_CONFIG_PARITY_NONE) {
|
||||||
|
config |= UART_PARITY_NONE;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||||
|
config |= UART_PARITY_EVEN;
|
||||||
|
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
|
||||||
|
config |= UART_PARITY_ODD;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this->data_bits_) {
|
||||||
|
case 5:
|
||||||
|
config |= SERIAL_DATA_5;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
config |= SERIAL_DATA_6;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
config |= SERIAL_DATA_7;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
config |= SERIAL_DATA_8;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->stop_bits_ == 1) {
|
||||||
|
config |= SERIAL_STOP_BIT_1;
|
||||||
|
} else {
|
||||||
|
config |= SERIAL_STOP_BIT_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RP2040UartComponent::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up UART bus...");
|
||||||
|
|
||||||
|
uint16_t config = get_config();
|
||||||
|
|
||||||
|
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});
|
||||||
|
constexpr uint32_t valid_tx_uart_1 = __bitset({4, 8, 20, 24});
|
||||||
|
|
||||||
|
constexpr uint32_t valid_rx_uart_0 = __bitset({1, 13, 17, 29});
|
||||||
|
constexpr uint32_t valid_rx_uart_1 = __bitset({5, 9, 21, 25});
|
||||||
|
|
||||||
|
int8_t tx_hw = -1;
|
||||||
|
int8_t rx_hw = -1;
|
||||||
|
|
||||||
|
if (this->tx_pin_ != nullptr) {
|
||||||
|
if (this->tx_pin_->is_inverted()) {
|
||||||
|
ESP_LOGD(TAG, "An inverted TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
|
||||||
|
} else {
|
||||||
|
if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_0) != 0) {
|
||||||
|
tx_hw = 0;
|
||||||
|
} else if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_1) != 0) {
|
||||||
|
tx_hw = 1;
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->rx_pin_ != nullptr) {
|
||||||
|
if (this->rx_pin_->is_inverted()) {
|
||||||
|
ESP_LOGD(TAG, "An inverted RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
|
||||||
|
} else {
|
||||||
|
if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_0) != 0) {
|
||||||
|
rx_hw = 0;
|
||||||
|
} else if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_1) != 0) {
|
||||||
|
rx_hw = 1;
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_LOGGER
|
||||||
|
if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) {
|
||||||
|
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw);
|
||||||
|
tx_hw = -1;
|
||||||
|
rx_hw = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) {
|
||||||
|
ESP_LOGV(TAG, "Using SerialPIO");
|
||||||
|
pin_size_t tx = this->tx_pin_ == nullptr ? SerialPIO::NOPIN : this->tx_pin_->get_pin();
|
||||||
|
pin_size_t rx = this->rx_pin_ == nullptr ? SerialPIO::NOPIN : this->rx_pin_->get_pin();
|
||||||
|
auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
serial->begin(this->baud_rate_, config);
|
||||||
|
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
|
||||||
|
gpio_set_outover(tx, GPIO_OVERRIDE_INVERT);
|
||||||
|
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
|
||||||
|
gpio_set_inover(rx, GPIO_OVERRIDE_INVERT);
|
||||||
|
this->serial_ = serial;
|
||||||
|
} else {
|
||||||
|
ESP_LOGV(TAG, "Using Hardware Serial");
|
||||||
|
SerialUART *serial;
|
||||||
|
if (tx_hw == 0) {
|
||||||
|
serial = &Serial1;
|
||||||
|
} else {
|
||||||
|
serial = &Serial2;
|
||||||
|
}
|
||||||
|
serial->setTX(this->tx_pin_->get_pin());
|
||||||
|
serial->setRX(this->rx_pin_->get_pin());
|
||||||
|
serial->setFIFOSize(this->rx_buffer_size_);
|
||||||
|
serial->begin(this->baud_rate_, config);
|
||||||
|
this->serial_ = serial;
|
||||||
|
this->hw_serial_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RP2040UartComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "UART Bus:");
|
||||||
|
LOG_PIN(" TX Pin: ", tx_pin_);
|
||||||
|
LOG_PIN(" RX Pin: ", rx_pin_);
|
||||||
|
if (this->rx_pin_ != nullptr) {
|
||||||
|
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
|
||||||
|
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
|
||||||
|
if (this->hw_serial_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Using hardware serial");
|
||||||
|
} else {
|
||||||
|
ESP_LOGCONFIG(TAG, " Using SerialPIO");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RP2040UartComponent::write_array(const uint8_t *data, size_t len) {
|
||||||
|
this->serial_->write(data, len);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
bool RP2040UartComponent::peek_byte(uint8_t *data) {
|
||||||
|
if (!this->check_read_timeout_())
|
||||||
|
return false;
|
||||||
|
*data = this->serial_->peek();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool RP2040UartComponent::read_array(uint8_t *data, size_t len) {
|
||||||
|
if (!this->check_read_timeout_(len))
|
||||||
|
return false;
|
||||||
|
this->serial_->readBytes(data, len);
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int RP2040UartComponent::available() { return this->serial_->available(); }
|
||||||
|
void RP2040UartComponent::flush() {
|
||||||
|
ESP_LOGVV(TAG, " Flushing...");
|
||||||
|
this->serial_->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RP2040
|
||||||
43
components/uart/uart_component_rp2040.h
Normal file
43
components/uart/uart_component_rp2040.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
|
||||||
|
#include <SerialPIO.h>
|
||||||
|
#include <SerialUART.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
class RP2040UartComponent : public UARTComponent, public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||||
|
|
||||||
|
void write_array(const uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
bool peek_byte(uint8_t *data) override;
|
||||||
|
bool read_array(uint8_t *data, size_t len) override;
|
||||||
|
|
||||||
|
int available() override;
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
uint16_t get_config();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void check_logger_conflict() override {}
|
||||||
|
bool hw_serial_{false};
|
||||||
|
|
||||||
|
HardwareSerial *serial_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RP2040
|
||||||
202
components/uart/uart_debugger.cpp
Normal file
202
components/uart/uart_debugger.cpp
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "uart_debugger.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
static const char *const TAG = "uart_debug";
|
||||||
|
|
||||||
|
UARTDebugger::UARTDebugger(UARTComponent *parent) {
|
||||||
|
parent->add_debug_callback([this](UARTDirection direction, uint8_t byte) {
|
||||||
|
if (!this->is_my_direction_(direction) || this->is_recursive_()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->trigger_after_direction_change_(direction);
|
||||||
|
this->store_byte_(direction, byte);
|
||||||
|
this->trigger_after_delimiter_(byte);
|
||||||
|
this->trigger_after_bytes_();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebugger::loop() { this->trigger_after_timeout_(); }
|
||||||
|
|
||||||
|
bool UARTDebugger::is_my_direction_(UARTDirection direction) {
|
||||||
|
return this->for_direction_ == UART_DIRECTION_BOTH || this->for_direction_ == direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UARTDebugger::is_recursive_() { return this->is_triggering_; }
|
||||||
|
|
||||||
|
void UARTDebugger::trigger_after_direction_change_(UARTDirection direction) {
|
||||||
|
if (this->has_buffered_bytes_() && this->for_direction_ == UART_DIRECTION_BOTH &&
|
||||||
|
this->last_direction_ != direction) {
|
||||||
|
this->fire_trigger_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) {
|
||||||
|
this->bytes_.push_back(byte);
|
||||||
|
this->last_direction_ = direction;
|
||||||
|
this->last_time_ = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebugger::trigger_after_delimiter_(uint8_t byte) {
|
||||||
|
if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->after_delimiter_[this->after_delimiter_pos_] != byte) {
|
||||||
|
this->after_delimiter_pos_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->after_delimiter_pos_++;
|
||||||
|
if (this->after_delimiter_pos_ == this->after_delimiter_.size()) {
|
||||||
|
this->fire_trigger_();
|
||||||
|
this->after_delimiter_pos_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebugger::trigger_after_bytes_() {
|
||||||
|
if (this->has_buffered_bytes_() && this->after_bytes_ > 0 && this->bytes_.size() >= this->after_bytes_) {
|
||||||
|
this->fire_trigger_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebugger::trigger_after_timeout_() {
|
||||||
|
if (this->has_buffered_bytes_() && this->after_timeout_ > 0 && millis() - this->last_time_ >= this->after_timeout_) {
|
||||||
|
this->fire_trigger_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); }
|
||||||
|
|
||||||
|
void UARTDebugger::fire_trigger_() {
|
||||||
|
this->is_triggering_ = true;
|
||||||
|
trigger(this->last_direction_, this->bytes_);
|
||||||
|
this->bytes_.clear();
|
||||||
|
this->is_triggering_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDummyReceiver::loop() {
|
||||||
|
// Reading up to a limited number of bytes, to make sure that this loop()
|
||||||
|
// won't lock up the system on a continuous incoming stream of bytes.
|
||||||
|
uint8_t data;
|
||||||
|
int count = 50;
|
||||||
|
while (this->available() && count--) {
|
||||||
|
this->read_byte(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the upcoming log functions, a delay was added after all log calls.
|
||||||
|
// This is done to allow the system to ship the log lines via the API
|
||||||
|
// TCP connection(s). Without these delays, debug log lines could go
|
||||||
|
// missing when UART devices block the main loop for too long.
|
||||||
|
|
||||||
|
void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) {
|
||||||
|
std::string res;
|
||||||
|
if (direction == UART_DIRECTION_RX) {
|
||||||
|
res += "<<< ";
|
||||||
|
} else {
|
||||||
|
res += ">>> ";
|
||||||
|
}
|
||||||
|
size_t len = bytes.size();
|
||||||
|
char buf[5];
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
res += separator;
|
||||||
|
}
|
||||||
|
sprintf(buf, "%02X", bytes[i]);
|
||||||
|
res += buf;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "%s", res.c_str());
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes) {
|
||||||
|
std::string res;
|
||||||
|
if (direction == UART_DIRECTION_RX) {
|
||||||
|
res += "<<< \"";
|
||||||
|
} else {
|
||||||
|
res += ">>> \"";
|
||||||
|
}
|
||||||
|
size_t len = bytes.size();
|
||||||
|
char buf[5];
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (bytes[i] == 7) {
|
||||||
|
res += "\\a";
|
||||||
|
} else if (bytes[i] == 8) {
|
||||||
|
res += "\\b";
|
||||||
|
} else if (bytes[i] == 9) {
|
||||||
|
res += "\\t";
|
||||||
|
} else if (bytes[i] == 10) {
|
||||||
|
res += "\\n";
|
||||||
|
} else if (bytes[i] == 11) {
|
||||||
|
res += "\\v";
|
||||||
|
} else if (bytes[i] == 12) {
|
||||||
|
res += "\\f";
|
||||||
|
} else if (bytes[i] == 13) {
|
||||||
|
res += "\\r";
|
||||||
|
} else if (bytes[i] == 27) {
|
||||||
|
res += "\\e";
|
||||||
|
} else if (bytes[i] == 34) {
|
||||||
|
res += "\\\"";
|
||||||
|
} else if (bytes[i] == 39) {
|
||||||
|
res += "\\'";
|
||||||
|
} else if (bytes[i] == 92) {
|
||||||
|
res += "\\\\";
|
||||||
|
} else if (bytes[i] < 32 || bytes[i] > 127) {
|
||||||
|
sprintf(buf, "\\x%02X", bytes[i]);
|
||||||
|
res += buf;
|
||||||
|
} else {
|
||||||
|
res += bytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res += '"';
|
||||||
|
ESP_LOGD(TAG, "%s", res.c_str());
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) {
|
||||||
|
std::string res;
|
||||||
|
size_t len = bytes.size();
|
||||||
|
if (direction == UART_DIRECTION_RX) {
|
||||||
|
res += "<<< ";
|
||||||
|
} else {
|
||||||
|
res += ">>> ";
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
res += separator;
|
||||||
|
}
|
||||||
|
res += to_string(bytes[i]);
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "%s", res.c_str());
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) {
|
||||||
|
std::string res;
|
||||||
|
size_t len = bytes.size();
|
||||||
|
if (direction == UART_DIRECTION_RX) {
|
||||||
|
res += "<<< ";
|
||||||
|
} else {
|
||||||
|
res += ">>> ";
|
||||||
|
}
|
||||||
|
char buf[20];
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
res += separator;
|
||||||
|
}
|
||||||
|
sprintf(buf, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]);
|
||||||
|
res += buf;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "%s", res.c_str());
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
||||||
101
components/uart/uart_debugger.h
Normal file
101
components/uart/uart_debugger.h
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#ifdef USE_UART_DEBUGGER
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "uart.h"
|
||||||
|
#include "uart_component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace uart {
|
||||||
|
|
||||||
|
/// The UARTDebugger class adds debugging support to a UART bus.
|
||||||
|
///
|
||||||
|
/// It accumulates bytes that travel over the UART bus and triggers one or
|
||||||
|
/// more actions that can log the data at an appropriate time. What
|
||||||
|
/// 'appropriate time' means exactly, is determined by a number of
|
||||||
|
/// configurable constraints. E.g. when a given number of bytes is gathered
|
||||||
|
/// and/or when no more data has been seen for a given time interval.
|
||||||
|
class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector<uint8_t>> {
|
||||||
|
public:
|
||||||
|
explicit UARTDebugger(UARTComponent *parent);
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
/// Set the direction in which to inspect the bytes: incoming, outgoing
|
||||||
|
/// or both. When debugging in both directions, logging will be triggered
|
||||||
|
/// when the direction of the data stream changes.
|
||||||
|
void set_direction(UARTDirection direction) { this->for_direction_ = direction; }
|
||||||
|
|
||||||
|
/// Set the maximum number of bytes to accumulate. When the number of bytes
|
||||||
|
/// is reached, logging will be triggered.
|
||||||
|
void set_after_bytes(size_t size) { this->after_bytes_ = size; }
|
||||||
|
|
||||||
|
/// Set a timeout for the data stream. When no new bytes are seen during
|
||||||
|
/// this timeout, logging will be triggered.
|
||||||
|
void set_after_timeout(uint32_t timeout) { this->after_timeout_ = timeout; }
|
||||||
|
|
||||||
|
/// Add a delimiter byte. This can be called multiple times to setup a
|
||||||
|
/// multi-byte delimiter (a typical example would be '\r\n').
|
||||||
|
/// When the constructed byte sequence is found in the data stream,
|
||||||
|
/// logging will be triggered.
|
||||||
|
void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UARTDirection for_direction_;
|
||||||
|
UARTDirection last_direction_{};
|
||||||
|
std::vector<uint8_t> bytes_{};
|
||||||
|
size_t after_bytes_;
|
||||||
|
uint32_t after_timeout_;
|
||||||
|
uint32_t last_time_{};
|
||||||
|
std::vector<uint8_t> after_delimiter_{};
|
||||||
|
size_t after_delimiter_pos_{};
|
||||||
|
bool is_triggering_{false};
|
||||||
|
|
||||||
|
bool is_my_direction_(UARTDirection direction);
|
||||||
|
bool is_recursive_();
|
||||||
|
void store_byte_(UARTDirection direction, uint8_t byte);
|
||||||
|
void trigger_after_direction_change_(UARTDirection direction);
|
||||||
|
void trigger_after_delimiter_(uint8_t byte);
|
||||||
|
void trigger_after_bytes_();
|
||||||
|
void trigger_after_timeout_();
|
||||||
|
bool has_buffered_bytes_();
|
||||||
|
void fire_trigger_();
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This UARTDevice is used by the serial debugger to read data from a
|
||||||
|
/// serial interface when the 'dummy_receiver' option is enabled.
|
||||||
|
/// The data are not stored, nor processed. This is most useful when the
|
||||||
|
/// debugger is used to reverse engineer a serial protocol, for which no
|
||||||
|
/// specific UARTDevice implementation exists (yet), but for which the
|
||||||
|
/// incoming bytes must be read to drive the debugger.
|
||||||
|
class UARTDummyReceiver : public Component, public UARTDevice {
|
||||||
|
public:
|
||||||
|
UARTDummyReceiver(UARTComponent *parent) : UARTDevice(parent) {}
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This class contains some static methods, that can be used to easily
|
||||||
|
/// create a logging action for the debugger.
|
||||||
|
class UARTDebug {
|
||||||
|
public:
|
||||||
|
/// Log the bytes as hex values, separated by the provided separator
|
||||||
|
/// character.
|
||||||
|
static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator);
|
||||||
|
|
||||||
|
/// Log the bytes as string values, escaping unprintable characters.
|
||||||
|
static void log_string(UARTDirection direction, std::vector<uint8_t> bytes);
|
||||||
|
|
||||||
|
/// Log the bytes as integer values, separated by the provided separator
|
||||||
|
/// character.
|
||||||
|
static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator);
|
||||||
|
|
||||||
|
/// Log the bytes as '<binary> (<hex>)' values, separated by the provided
|
||||||
|
/// separator.
|
||||||
|
static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace uart
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
||||||
253
truma.yaml
Normal file
253
truma.yaml
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
esphome:
|
||||||
|
name: "esphome-truma-all-config"
|
||||||
|
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
# read time from external source (connected via I2C)
|
||||||
|
- ds1307.read_time
|
||||||
|
# wait 90 seconds for Truma CP Plus connection to init
|
||||||
|
- delay: 90 seconds
|
||||||
|
# update CP Plus clock
|
||||||
|
- truma_inetbox.clock.set
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: github://Fabian-Schmidt/esphome-truma_inetbox
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: mhetesp32devkit
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
sda: 14
|
||||||
|
scl: 27
|
||||||
|
scan: false
|
||||||
|
id: i2c_bus_a
|
||||||
|
|
||||||
|
time:
|
||||||
|
- platform: ds1307
|
||||||
|
update_interval: never
|
||||||
|
- platform: sntp
|
||||||
|
on_time_sync:
|
||||||
|
- ds1307.write_time
|
||||||
|
# wait 90 seconds for Truma CP Plus connection to init
|
||||||
|
- delay: 90 seconds
|
||||||
|
- truma_inetbox.clock.set
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: lin_uart_bus
|
||||||
|
tx_pin: 17
|
||||||
|
rx_pin: 16
|
||||||
|
baud_rate: 9600
|
||||||
|
data_bits: 8
|
||||||
|
parity: NONE
|
||||||
|
stop_bits: 2
|
||||||
|
|
||||||
|
truma_inetbox:
|
||||||
|
uart_id: lin_uart_bus
|
||||||
|
cs_pin: 5
|
||||||
|
fault_pin: 18
|
||||||
|
# Advanced users can use `on_heater_message` action. The heater data is in the `message` variable.
|
||||||
|
on_heater_message:
|
||||||
|
then:
|
||||||
|
- logger.log: "Message from CP Plus."
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return message->operating_status == truma_inetbox::OperatingStatus::OPERATING_STATUS_OFF;
|
||||||
|
then:
|
||||||
|
- logger.log: "Heater is off."
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "CP Plus alive"
|
||||||
|
type: CP_PLUS_CONNECTED
|
||||||
|
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Room Heater active"
|
||||||
|
type: HEATER_ROOM
|
||||||
|
id: HEATER_ROOM
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Water Heater active"
|
||||||
|
type: HEATER_WATER
|
||||||
|
id: HEATER_WATER
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Heater mode Gas"
|
||||||
|
type: HEATER_GAS
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Heater mode Mix 1"
|
||||||
|
type: HEATER_MIX_1
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Heater mode Mix 2"
|
||||||
|
type: HEATER_MIX_2
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Heater mode Elec"
|
||||||
|
type: HEATER_ELECTRICITY
|
||||||
|
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Timer active"
|
||||||
|
type: TIMER_ACTIVE
|
||||||
|
id: TIMER_ACTIVE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Timer Room Heater active"
|
||||||
|
type: TIMER_ROOM
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Timer Water Heater active"
|
||||||
|
type: TIMER_WATER
|
||||||
|
|
||||||
|
climate:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Truma Room"
|
||||||
|
type: ROOM
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Truma Water"
|
||||||
|
type: WATER
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Room Temperature"
|
||||||
|
type: TARGET_ROOM_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Water Temperature"
|
||||||
|
type: TARGET_WATER_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "electric power level"
|
||||||
|
type: ELECTRIC_POWER_LEVEL
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Current Room Temperature"
|
||||||
|
type: CURRENT_ROOM_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Current Water Temperature"
|
||||||
|
type: CURRENT_WATER_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Room Temperature"
|
||||||
|
type: TARGET_ROOM_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Target Water Temperature"
|
||||||
|
type: TARGET_WATER_TEMPERATURE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Heating mode"
|
||||||
|
type: HEATING_MODE
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "electric power level"
|
||||||
|
type: ELECTRIC_POWER_LEVEL
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Energy mix"
|
||||||
|
type: ENERGY_MIX
|
||||||
|
- platform: truma_inetbox
|
||||||
|
name: "Operating status"
|
||||||
|
type: OPERATING_STATUS
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: "Activate Room Heater"
|
||||||
|
lambda: |-
|
||||||
|
return id(HEATER_ROOM).state;
|
||||||
|
turn_on_action:
|
||||||
|
- truma_inetbox.heater.set_target_room_temperature:
|
||||||
|
# You can use lambda functions
|
||||||
|
temperature: !lambda |-
|
||||||
|
return 16;
|
||||||
|
# Optional set heating mode: `"OFF"`, `ECO`, `HIGH`, `BOOST`
|
||||||
|
heating_mode: ECO
|
||||||
|
turn_off_action:
|
||||||
|
- truma_inetbox.heater.set_target_room_temperature:
|
||||||
|
# Disable heater by setting temperature to `0`.
|
||||||
|
temperature: 0
|
||||||
|
- platform: template
|
||||||
|
name: "Activate Water Heater"
|
||||||
|
lambda: |-
|
||||||
|
return id(HEATER_WATER).state;
|
||||||
|
turn_on_action:
|
||||||
|
- truma_inetbox.heater.set_target_water_temperature:
|
||||||
|
# Set water temp as number: `0`, `40`, `60`, `80`
|
||||||
|
temperature: 40
|
||||||
|
turn_off_action:
|
||||||
|
- truma_inetbox.heater.set_target_water_temperature:
|
||||||
|
# Disable heater by setting temperature to `0`.
|
||||||
|
temperature: 0
|
||||||
|
- platform: template
|
||||||
|
name: "Activate Water Heater (enum)"
|
||||||
|
lambda: |-
|
||||||
|
return id(HEATER_WATER).state;
|
||||||
|
turn_on_action:
|
||||||
|
- truma_inetbox.heater.set_target_water_temperature_enum:
|
||||||
|
# Set water temp as text: `"OFF"`, `ECO`, `HIGH`, `BOOST`
|
||||||
|
temperature: ECO
|
||||||
|
turn_off_action:
|
||||||
|
# You can also use the simplified syntax.
|
||||||
|
- truma_inetbox.heater.set_target_water_temperature_enum: "OFF"
|
||||||
|
- platform: template
|
||||||
|
name: "Active Timer"
|
||||||
|
lambda: |-
|
||||||
|
return id(TIMER_ACTIVE).state;
|
||||||
|
turn_on_action:
|
||||||
|
- truma_inetbox.timer.activate:
|
||||||
|
start: 7:00
|
||||||
|
stop: 9:30
|
||||||
|
# Required: Set room temp to a number between 5 and 30
|
||||||
|
room_temperature: 13
|
||||||
|
# Optional: Set heating mode: `"OFF"`, `ECO`, `HIGH`, `BOOST`
|
||||||
|
heating_mode: ECO
|
||||||
|
# Optional: Set water temp as number: `0`, `40`, `60`, `80`
|
||||||
|
water_temperature: 0
|
||||||
|
# Optional: Set energy mix to: `GAS`, `MIX`, `ELECTRICITY`
|
||||||
|
energy_mix: GAS
|
||||||
|
# Optional: Set electricity level to `0`, `900`, `1800`
|
||||||
|
watt: 0
|
||||||
|
|
||||||
|
turn_off_action:
|
||||||
|
# You can also use the simplified syntax.
|
||||||
|
- truma_inetbox.timer.disable
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Energy mix GAS only"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_energy_mix:
|
||||||
|
# Set energy mix to: `GAS`, `MIX`, `ELECTRICITY`
|
||||||
|
energy_mix: GAS
|
||||||
|
- platform: template
|
||||||
|
name: "Energy mix MIX 1"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_energy_mix:
|
||||||
|
energy_mix: MIX
|
||||||
|
# Set electricity level to `0`, `900`, `1800`
|
||||||
|
watt: 900W
|
||||||
|
- platform: template
|
||||||
|
name: "Energy mix MIX 2"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_energy_mix:
|
||||||
|
energy_mix: MIX
|
||||||
|
watt: 1800
|
||||||
|
- platform: template
|
||||||
|
name: "Energy mix ELECTRICITY only"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_energy_mix:
|
||||||
|
energy_mix: ELECTRICITY
|
||||||
|
watt: 1800W
|
||||||
|
- platform: template
|
||||||
|
name: "Set electric power level to 0 Watt"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_electric_power_level: 0
|
||||||
|
- platform: template
|
||||||
|
name: "Set electric power level to 900 Watt"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_electric_power_level: 900
|
||||||
|
- platform: template
|
||||||
|
name: "Set electric power level to 1800 Watt"
|
||||||
|
on_press:
|
||||||
|
- truma_inetbox.heater.set_electric_power_level: 1800
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "Fallback Hotspot"
|
||||||
|
password: "Sw9g4XJtoyrn"
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 80
|
||||||
|
local: true
|
||||||
|
version: 2
|
||||||
|
include_internal: true
|
||||||
Loading…
x
Reference in New Issue
Block a user