Repository: luar123/zigbee_esphome Branch: master Commit: 41761b894b8e Files: 37 Total size: 215.2 KB Directory structure: gitextract_o48vfjhm/ ├── .clang-format ├── .clang-tidy ├── .editorconfig ├── .flake8 ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .yamllint ├── README.md ├── components/ │ └── zigbee/ │ ├── __init__.py │ ├── automation.cpp │ ├── automation.h │ ├── const.py │ ├── esp_zb_event.h │ ├── files_to_parse/ │ │ └── parse_zigbee_headers.py │ ├── partitions_zb.csv │ ├── time/ │ │ ├── __init__.py │ │ ├── zigbee_time.cpp │ │ └── zigbee_time.h │ ├── types.py │ ├── zigbee.cpp │ ├── zigbee.h │ ├── zigbee_attribute.cpp │ ├── zigbee_attribute.h │ ├── zigbee_const.py │ ├── zigbee_ep.py │ ├── zigbee_helpers.c │ ├── zigbee_helpers.h │ └── zigbee_pre_build.py.script ├── example_aht10_esp32h2.yaml ├── example_esp32c6.yaml ├── example_esp32h2.yaml ├── example_esp32h2_basic.yaml ├── example_time.yml ├── partitions_zb.csv └── pyproject.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ 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: '^' 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 ================================================ FILE: .clang-tidy ================================================ --- Checks: >- *, -abseil-*, -altera-*, -android-*, -boost-*, -bugprone-easily-swappable-parameters, -bugprone-implicit-widening-of-multiplication-result, -bugprone-narrowing-conversions, -bugprone-signed-char-misuse, -cert-dcl50-cpp, -cert-err33-c, -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-ignored-optimization-argument, -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-prefer-member-initializer, -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, -cppcoreguidelines-virtual-class-destructor, -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-container-data-pointer, -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-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 - key: readability-identifier-length.MinimumVariableNameLength value: 0 - key: readability-identifier-length.MinimumParameterNameLength value: 0 - key: readability-identifier-length.MinimumLoopCounterNameLength value: 0 ================================================ FILE: .editorconfig ================================================ root = true # general [*] end_of_line = lf insert_final_newline = true charset = utf-8 # python [*.py] indent_style = space indent_size = 4 # C++ [*.{cpp,h,tcc}] indent_style = space indent_size = 2 # Web [*.{js,html,css}] indent_style = space indent_size = 2 # YAML [*.{yaml,yml}] indent_style = space indent_size = 2 quote_type = double # JSON [*.json] indent_style = space indent_size = 2 ================================================ FILE: .flake8 ================================================ [flake8] max-line-length = 120 # Following 4 for black compatibility # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # TODO fix flake8 # D100 Missing docstring in public module # D101 Missing docstring in public class # D102 Missing docstring in public method # D103 Missing docstring in public function # D104 Missing docstring in public package # D105 Missing docstring in magic method # D107 Missing docstring in __init__ # D200 One-line docstring should fit on one line with quotes # D205 1 blank line required between summary line and description # D209 Multi-line docstring closing quotes should be on a separate line # D400 First line should end with a period # D401 First line should be in imperative mood ignore = E501, W503, E203, D202, D100, D101, D102, D103, D104, D105, D107, D200, D205, D209, D400, D401, exclude = api_pb2.py ================================================ FILE: .gitattributes ================================================ # Normalize line endings to LF in the repository * text eol=lf *.png binary ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms #github: luar123 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] #patreon: # Replace with a single Patreon username #open_collective: # Replace with a single Open Collective username #ko_fi: # Replace with a single Ko-fi username #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry #liberapay: # Replace with a single Liberapay username #issuehunt: # Replace with a single IssueHunt username #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry #polar: # Replace with a single Polar username buy_me_a_coffee: luar123 #thanks_dev: # Replace with a single thanks.dev username #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ # 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 /.vscode/ __pycache__/ /components/zigbee/files_to_parse/*.c /components/zigbee/files_to_parse/*.h /components/zigbee/files_to_parse/zigbee_const.py ================================================ FILE: .pre-commit-config.yaml ================================================ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.5.4 hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.4.2 hooks: - id: black args: - --safe - --quiet files: ^((esphome|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.1.1 files: ^(esphome|tests)/.+\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 hooks: - id: no-commit-to-branch args: - --branch=dev - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade rev: v3.15.2 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-clang-format rev: v13.0.1 hooks: - id: clang-format types_or: [c, c++] - repo: local hooks: - id: pylint name: pylint entry: pylint language: system types: [python] ================================================ FILE: .yamllint ================================================ --- extends: default ignore-from-file: .gitignore rules: document-start: disable empty-lines: level: error max: 1 max-start: 0 max-end: 1 indentation: level: error spaces: 2 indent-sequences: true check-multi-line-strings: false line-length: disable truthy: disable ================================================ FILE: README.md ================================================ > [!TIP] > **New simple Mode! No more endpoint definitions needed.** > > I started to implement the automated endpoint definition generation, see basic mode section for details. > [!Important] > **Please help to collect working cluster definitions [here](https://github.com/luar123/zigbee_esphome/discussions/22).** > > **If something is not working please check the [troubleshooting](#troubleshooting) section first. Config validation is not complete. Always consult [Zigbee Cluster Library](https://csa-iot.org/wp-content/uploads/2022/01/07-5123-08-Zigbee-Cluster-Library-1.pdf) for cluster definitions** # ESPHome ZigBee external component External ZigBee component for ESPHome. ## Features - Automated generation of zigbee definition for lights, switches, sensors and binary sensors (see basic mode) - Definition of endpoints, clusters and attributes supported by esp-zigbee-sdk 1.6 - Set attributes action - Manual report action - Reset zigbee action - Join trigger - Attribute received trigger - Time sync with coordinator - Custom clusters and attributes - (normal, binary, text) sensors, switches and lights can be connected to attributes without need for lambdas/actions - Wifi co-existence on ESP32-C6 and ESP32-C5 - Deep-sleep should work - Not tested: groups - Time sync with coordinator - Router ## Limitations - No coordinator devices - Attribute set action works only with numeric types and character string - Attribute OnValue trigger works only with numeric types - Reporting can be enabled, but not configured - No control devices like switches ([workaround](https://github.com/luar123/zigbee_esphome/discussions/18#discussioncomment-11875376)) - Needs esp-idf >=5.1.4 - Needs esphome >=2025.7 - scenes not implemented - Officially the zigbee stack supports only 10 endpoints. however, this is not enforced and at least for sensor endpoints more than 10 seem to work. More then 10 light endpoints will crash! - zigbee2mqtt: Only one light is supported without creating a custom converter/definition - zigbee2mqtt: Analog input cluster (used for sensors) is supported by 2025 October release, but ignores type and unit - ZHA: Analog input cluster (used for sensors) without unit/type is ignored - ZHA: Minimum reporting interval is set to high values (30s) for some sensors and can't be changed. Keep that in mind if reporting seems not to work properly. ## ToDo List (Short-Mid term) - Light effects (through identify cluster commands) - more components to support basic mode ## Not planned (feel free to submit a pull request) - [Zigbee ZCL OTA Upgrade Cluster](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32/user-guide/zcl_ota_upgrade.html) and related [OTA API for ESP Zigbee SDK](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32/api-reference/esp_zigbee_ota.html) to allow OTA (over-the-air) firmware updates via Zigbee - Coordinator devices - Binding config in yaml - Reporting config in yaml - Control device support like switches ([workaround](https://github.com/luar123/zigbee_esphome/discussions/18#discussioncomment-11875376)) ## Usage Include external component: ``` external_components: - source: github://luar123/zigbee_esphome components: [zigbee] zigbee: components: all # to add all supported components ... ``` ### Configuration variables - **id** (Optional, string): Manually specify the ID for code generation. - **name** (Optional, string): Zigbee Model Identifier in basic cluster. Used by coordinator to match custom converters. Defaults to `esphome.name` - **manufacturer** (Optional, string): Zigbee Manufacturer Name in basic cluster. Used by coordinator to match custom converters. Defaults to `"esphome"` - **date** (Optional, string): Date Code in basic cluster. Defaults to build time - **power_supply** (Optional, int): Zigbee Power Source in basic cluster. Defaults to `0` = unknown - `1` = single phase mains (USB) - `2` = three phase mains - `3` = battery - **version** (Optional, int): Zigbee App Version in basic cluster. Defaults to `0` - **area** (Optional, int): Zigbee Physical Environment in basic cluster. See ZCL. Defaults to `0` = unknown - **router** (Optional, bool): Create a router device instead of an end device. Defaults to `false` - **device_version** (Optional, int): Set the Home Automation Profile device version. Custom values might be needed for compatibility with some vendors. Defaults to `0` - **trust_center_key** (Optional, bind_key): Set custom trust center key. 32 digits hex number. - **debug** (Optional, bool): Print zigbee stack debug messages. Defaults to `false` - **components** (Optional, string|list): `all`: add definitions for all supported components that have a name and are not marked as internal. - None: Add no definitions (default). - List of component ids: Add only those. Can be combined with manual definitions in endpoints - **as_generic** (Optional, bool): Use generic/basic clusters where possible. Currently sensors and switches. Defaults to false - **endpoints** (Optional, list): endpoint list for advanced definitions. See examples [Todo] ### Basic mode By adding `components: all` the endpoint definition is generated automatically. Currently [sensor](https://esphome.io/components/sensor/), [binary_sensor](https://esphome.io/components/binary_sensor/), [light](https://esphome.io/components/light/) and [switch](https://esphome.io/components/switch/) ESPHome components are supported. Because this is an [external component](https://esphome.io/components/external_components/) the whole implementation is a bit hacky and likely to fail with some setups. Also it is not possible to tweak the generated definitions. Each entity creates a new endpoint. For sensors the unit/type is set automatically. Please note that these definitions are not complete. Feel free to open an issue or pull request (see zigbee_ep.py) | ESPHome Entity | Zigbee Cluster | | --------------- | ------------------------------------------------------------------ | | `light` | `light` | | `switch` | `on_off`, `binary_output` | | `binary_sensor` | `binary_input` | | `sensor` | `analog_input` or mapped to specific (e.g. `temperature`) clusters | **Basic example**: ``` zigbee: id: "zb" components: all ``` ### Advanced mode Endpoint/Cluster definitions can be defined manually. Can be combined with automated definition. Advanced example: ``` zigbee: id: "zb" endpoints: - num: 1 device_type: COLOR_DIMMABLE_LIGHT clusters: - id: ON_OFF attributes: - attribute_id: 0 type: bool on_value: then: - light.control: id: light_1 state: !lambda "return (bool)x;" - id: LEVEL_CONTROL attributes: - attribute_id: 0 type: U8 on_value: then: - light.control: id: light_1 brightness: !lambda "return ((float)x)/255;" - device_type: TEMPERATURE_SENSOR num: 2 clusters: - id: REL_HUMIDITY_MEASUREMENT attributes: - attribute_id: 0 id: hum_attr type: U16 report: true value: 200 - id: TEMP_MEASUREMENT attributes: - attribute_id: 0x0 type: S16 report: true value: 100 device: temp_sensor_id scale: 100 - device_type: HEATING_COOLING_UNIT num: 3 clusters: - id: THERMOSTAT role: "Client" attributes: - attribute_id: 0x0008 # PIHeatingDemand type: U8 value: 0 on_report: then: # The below lambda will be called with an argument # `ZigBeeReportData x` where ZigBeeReportData is defined as # # template struct ZigBeeReportData { # // Value of the attribute sent from server side. # T value; # // Address of device which sent this value. # esp_zb_zcl_addr_t src_address; # // Number of the endpoint on device which sent this value. # uint8_t src_endpoint; # }; # # And T is a C++ type matching the type of the attribute. - lambda: |- ESP_LOGD("main", "Received PIHeatingDemand | address type: 0x%02x; short addr: 0x%04x; ieee addr: %02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X", x.src_address.addr_type, x.src_address.u.short_addr, x.src_address.u.ieee_addr[7], x.src_address.u.ieee_addr[6], x.src_address.u.ieee_addr[5], x.src_address.u.ieee_addr[4], x.src_address.u.ieee_addr[3], x.src_address.u.ieee_addr[2], x.src_address.u.ieee_addr[1], x.src_address.u.ieee_addr[0]); - switch.control: id: relay_1_switch state: !lambda "return (x.value>10);" on_join: then: - logger.log: "Joined network" ``` ### Actions - `zigbee.setAttr` - `id`: id of attribute - `value`: attribute value. (can be a `lambda`) - only numeric or string types - `zigbee.report`: `id` of zigbee component - Manually send reports for all attributes with `report=true` - `zigbee.reportAttr`: `id` of zigbee_attribute component - Manually send report for attribute - `zigbee.reset`: `id` of zigbee component - Reset Zigbee Network and Device. Leave the current network and tries to join open networks. Examples: ``` on_value: then: - zigbee.setAttr: id: hum_attr value: !lambda "return x*100;" ``` ``` on_press: then: - zigbee.report: zb ``` ### Time sync Add a 'time' component with platform 'zigbee', e.g.: ``` zigbee: id: "zb" ... time: - platform: zigbee timezone: Europe/London on_time_sync: then: - logger.log: "Synchronized system clock" on_time: - seconds: /10 then: - logger.log: "Tick-tock 10 seconds" ``` ## Troubleshooting - Build errors - Try to run `esphome clean ` - Try to delete the `.esphome/build//` folder - ESP crashes - Try to erase completely with `esptool.py erase_flash` and flash again. - Make sure your configuration is valid. Config validation is not complete. Always consult [Zigbee Cluster Library](https://csa-iot.org/wp-content/uploads/2022/01/07-5123-08-Zigbee-Cluster-Library-1.pdf) for cluster definitions - Common issues are that attributes do not support reporting (try set `report: false`), use a different type, or are not readable/writable (see ZCL). - Zigbee is not working as expected - Whenever the cluster definition changed you need to re-interview and remove/add the device to your network. - Sometimes it helps to power-cycle the coordinator and restarting z2m. - Remove other endpoints. Sometimes coordinators struggle with multiple endpoints. ## Notes - I don't have much free time to work on this right now, so feel free to fork/improve/create PRs/etc. - At the moment, the C++ implementation is rather simple and generic. I tried to keep as much logic as possible in the python part. However, endpoints/clusters ~~/attributes~~ could also be classes, this would simplify the yaml setup but requires more sophisticated C++ code. - There is also a project with more advanced C++ zigbee libraries for esp32 that could be used here as well: https://github.com/Muk911/esphome/tree/main/esp32c6/hello-zigbee - [parse_zigbee_headers.py](components/zigbee/files_to_parse/parse_zigbee_headers.py) is used to create the python enums and C helper functions automatically from zigbee sdk headers. - Deprecated [custom zigbee component](https://github.com/luar123/esphome_zb_sensor) ## Example Zigbee device ESPHome Zigbee using only dev board or additionally [AHT10 Temperature+Humidity Sensor](https://next.esphome.io/components/sensor/aht10). ### Hardware Required - One development board with ESP32-H2, ESP32-C5 or ESP32-C6 SoC acting as Zigbee end-device (that you will load ESPHome with the example config to). - For example, the official [ESP32-H2-DevKitM-1](https://docs.espressif.com/projects/espressif-esp-dev-kits/en/latest/esp32h2/esp32-h2-devkitm-1/user_guide.html) development kit board. - [AHT10 Temperature+Humidity Sensor](https://next.esphome.io/components/sensor/aht10) connected to I2C pins (SDA: 12, SCL: 22) for the aht10 example. - A USB cable for power supply and programming. - (Optional) A USB-C cable to get ESP32 logs from the UART USB port (UART0). ### Build ESPHome Zigbee sensor - We will build [example_esp32h2.yaml](example_esp32h2.yaml) file. - Check [Getting Started with the ESPHome Command Line](https://esphome.io/guides/getting_started_command_line.html) tutorial to set up your dev environment. - Build with `esphome run example_esp32h2.yaml`. ## How to contribute **Please submit all PRs here** and not to https://github.com/luar123/esphome/tree/zigbee Use pre-commit hook by enabling you esphome environment first and then running `pre-commit install` in the git root foulder. If looking to contribute to this project, then suggest follow steps in these guides + look at issues in Espressif's ESP Zigbee SDK repository: - https://github.com/espressif/esp-zigbee-sdk/issues - https://github.com/firstcontributions/first-contributions/blob/master/README.md - https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md ## External documentation and reference > [!NOTE] > The official documentation and reference examples for the ESP Zigbee SDK can currently be obtained from Espressif: - [ESP32 Zigbee SDK Programming Guide](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32/) - [ESP32-H2 Application User Guide](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32h2/application.html) - [ESP32-C6 Application User Guide](https://docs.espressif.com/projects/esp-zigbee-sdk/en/latest/esp32c6/application.html) - [ESP-Zigbee-SDK Github repo](https://github.com/espressif/esp-zigbee-sdk) - [ESP-Zigbee-SDK examples](https://github.com/espressif/esp-zigbee-sdk/tree/main/examples/) - [Zigbee HA Example](https://github.com/espressif/esp-zigbee-sdk/tree/main/examples/esp_zigbee_HA_sample) - [Zigbee HA Light Bulb example](https://github.com/espressif/esp-zigbee-sdk/tree/main/examples/esp_zigbee_HA_sample/HA_on_off_light) - [Zigbee HA temperature sensor example](https://github.com/espressif/esp-zigbee-sdk/tree/main/examples/esp_zigbee_HA_sample/HA_temperature_sensor) - [Zigbee HA thermostat example](https://github.com/espressif/esp-zigbee-sdk/tree/main/examples/esp_zigbee_HA_sample/HA_thermostat) ================================================ FILE: components/zigbee/__init__.py ================================================ import datetime import inspect from pathlib import Path import re from esphome import automation import esphome.codegen as cg from esphome.components import text_sensor from esphome.components.esp32 import ( CONF_PARTITIONS, add_extra_script, add_idf_component, add_idf_sdkconfig_option, only_on_variant, ) from esphome.components.esp32.const import ( VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, ) import esphome.config_validation as cv from esphome.const import ( CONF_AP, CONF_AREA, CONF_COMPONENTS, CONF_DATE, CONF_DEBUG, CONF_DEVICE, CONF_ID, CONF_LAMBDA, CONF_MAX_LENGTH, CONF_NAME, CONF_ON_VALUE, CONF_POWER_SUPPLY, CONF_TRIGGER_ID, CONF_TYPE, CONF_VALUE, CONF_VERSION, CONF_WIFI, ) from esphome.core import CORE, EsphomeError import esphome.final_validate as fv from .const import ( CONF_ACCESS, CONF_AS_GENERIC, CONF_ATTRIBUTE_ID, CONF_ATTRIBUTES, CONF_CLUSTERS, CONF_DEVICE_TYPE, CONF_DEVICE_VERSION, CONF_ENDPOINTS, CONF_MANUFACTURER, CONF_NUM, CONF_ON_JOIN, CONF_ON_REPORT, CONF_REPORT, CONF_ROLE, CONF_ROUTER, CONF_SCALE, CONF_TRUST_CENTER_KEY, BinarySensor, Sensor, Switch, ) from .types import ( ReportAction, ReportAttrAction, ResetZigbeeAction, SetAttrAction, ZigBeeAttribute, ZigBeeComponent, ZigBeeOnReportTrigger, ZigBeeOnValueTrigger, ) from .zigbee_const import ATTR_ACCESS, ATTR_TYPE, CLUSTER_ID, CLUSTER_ROLE, DEVICE_ID from .zigbee_ep import create_ep try: from esphome.components.esp32 import require_vfs_select except ImportError: def require_vfs_select(): pass _supports_synchronous = ( "synchronous" in inspect.signature(automation.register_action).parameters ) def _register_action(name, action_type, schema, **kwargs): if _supports_synchronous: kwargs.setdefault("synchronous", True) else: kwargs.pop("synchronous", None) return automation.register_action(name, action_type, schema, **kwargs) DEPENDENCIES = ["esp32"] _CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation(CONF_ON_JOIN, "add_on_join_callback"), ) comp_ids = 0 # dummies for upstream compatibility BINARY_SENSOR_SCHEMA = cv.Schema({}) SENSOR_SCHEMA = cv.Schema({}) SWITCH_SCHEMA = cv.Schema({}) NUMBER_SCHEMA = cv.Schema({}) def validate_binary_sensor(x): return x def validate_sensor(x): return x def validate_switch(x): return x def validate_number(x): return x async def setup_binary_sensor(sensor, config): pass async def setup_sensor(sensor, config): pass async def setup_switch(switch, config): pass async def setup_number(number, config, min_value, max_value, step): pass def get_c_size(bits, options): return str([n for n in options if n >= int(bits)][0]) def get_c_type(attr_type): if attr_type == "BOOL": return cg.bool_ if attr_type == "SINGLE": return cg.float_ if attr_type == "DOUBLE": return cg.double if "STRING" in attr_type: return cg.std_string test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) if test and test.group(2): return getattr(cg, "uint" + get_c_size(test.group(2), [8, 16, 32, 64])) test = re.match(r"^S(\d{1,2})$", attr_type) if test and test.group(1): return getattr(cg, "int" + get_c_size(test.group(1), [16, 32, 64])) raise EsphomeError(f"Zigbee: type {attr_type} not supported or implemented") def get_cv_by_type(attr_type): if attr_type == "BOOL": return cv.boolean if attr_type in ["SEMI", "SINGLE", "DOUBLE"]: return cv.float_ if "STRING" in attr_type: return cv.string test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) if test and test.group(2): return cv.positive_int test = re.match(r"^S(\d{1,2})$", attr_type) if test and test.group(1): return cv.int_ raise cv.Invalid(f"Zigbee: type {attr_type} not supported or implemented") def get_default_by_type(attr_type): if "CHAR_STRING" == attr_type: return "" return 0 def validate_clusters(config): for attr in config.get(CONF_ATTRIBUTES): if isinstance(config.get(CONF_ID), int) and config.get(CONF_ID) >= 0xFC00: if not {CONF_TYPE, CONF_ACCESS, CONF_VALUE} <= attr.keys(): raise cv.Invalid( f"Parameters {CONF_TYPE}, {CONF_VALUE} and {CONF_ACCESS} are need for custom cluster." ) return config def validate_string_attributes(config): if "CHAR_STRING" == config[CONF_TYPE]: if CONF_MAX_LENGTH not in config.keys(): raise cv.Invalid( f"The '{CONF_MAX_LENGTH}' parameter is mandatory for string attributes." ) if config[CONF_MAX_LENGTH] == 0: config[CONF_MAX_LENGTH] = len(config.get(CONF_VALUE, "")) # Check that size of default value matches CONF_MAX_LENGTH if len(config[CONF_VALUE]) > config[CONF_MAX_LENGTH]: raise cv.Invalid( "The default value is larger than the maximum length of the string attribute." ) return config def validate_attributes(config): if CONF_VALUE in config: config[CONF_VALUE] = get_cv_by_type(config[CONF_TYPE])(config[CONF_VALUE]) else: config[CONF_VALUE] = get_default_by_type(config[CONF_TYPE]) config[CONF_ACCESS] = ( ATTR_ACCESS[config[CONF_ACCESS]] + config[CONF_REPORT] * 4 if CONF_ACCESS in config else 0 ) validate_string_attributes(config) if (CONF_ID not in config) and ( CONF_DEVICE in config or CONF_ON_VALUE in config or CONF_ON_REPORT in config ): config[CONF_ID] = cv.declare_id(ZigBeeAttribute)(None) elif all( i not in config for i in [CONF_ID, CONF_DEVICE, CONF_ON_VALUE, CONF_ON_REPORT] ) and (config[CONF_SCALE] != 1.0 or CONF_LAMBDA in config or config[CONF_REPORT]): raise cv.Invalid( f"Parameters {CONF_SCALE}', '{CONF_LAMBDA}' or '{CONF_REPORT}' are not allowed without '{CONF_ID}', '{CONF_DEVICE}', '{CONF_ON_VALUE}' or '{CONF_ON_REPORT}'." ) return config def final_validate(config): esp_conf = fv.full_config.get()["esp32"] if CONF_PARTITIONS in esp_conf: with open( CORE.relative_config_path(esp_conf[CONF_PARTITIONS]), encoding="utf8" ) as f: partitions = f.read() if ("zb_storage" not in partitions) and ("zb_fct" not in partitions): raise cv.Invalid( "Add \n'zb_storage, data, fat, , 16K,'\n'zb_fct, data, fat, , 1K,'\n to your custom partition table." ) else: raise cv.Invalid( f"Use '{CONF_PARTITIONS}' in esp32 to specify a custom partition table including zigbee partitions" ) if CONF_WIFI in fv.full_config.get(): if CONF_AP in fv.full_config.get()[CONF_WIFI]: raise cv.Invalid("Zigbee can't be used together with an Wifi Access Point.") global comp_ids # noqa: PLW0603 comp_ids = len(CORE.component_ids) return config FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) def _require_vfs_select(config): """Register VFS select requirement during config validation.""" # ZigBee uses esp_vfs_eventfd which requires VFS select support require_vfs_select() return config CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ZigBeeComponent), cv.Optional(CONF_NAME): cv.string, cv.Optional(CONF_MANUFACTURER, default="esphome"): cv.string, cv.Optional( CONF_DATE, default=datetime.datetime.now().strftime("%Y%m%d") ): cv.string, cv.Optional(CONF_POWER_SUPPLY, default=0): cv.int_, # make enum cv.Optional(CONF_VERSION, default=0): cv.int_, cv.Optional(CONF_AREA, default=0): cv.int_, # make enum cv.Optional(CONF_ROUTER, default=False): cv.boolean, cv.Optional(CONF_TRUST_CENTER_KEY): cv.bind_key, cv.Optional(CONF_DEVICE_VERSION): cv.int_, cv.Optional(CONF_DEBUG, default=False): cv.boolean, cv.Optional(CONF_COMPONENTS): cv.Any( cv.one_of("all", "none", lower=True), cv.ensure_list(cv.use_id(cg.EntityBase)), ), cv.Optional(CONF_AS_GENERIC, default=False): cv.boolean, cv.Optional(CONF_ENDPOINTS): cv.ensure_list( cv.Schema( { cv.Required(CONF_DEVICE_TYPE): cv.enum(DEVICE_ID, upper=True), cv.Optional(CONF_NUM): cv.int_range(1, 240), cv.Optional(CONF_CLUSTERS): cv.ensure_list( cv.Schema( { cv.Required(CONF_ID): cv.Any( cv.enum(CLUSTER_ID, upper=True), cv.int_range(0xFC00, 0xFFFF), ), cv.Optional(CONF_ROLE, default="Server"): cv.enum( CLUSTER_ROLE, upper=True ), cv.Optional(CONF_ATTRIBUTES): cv.ensure_list( cv.Schema( { cv.Optional(CONF_ID): cv.declare_id( ZigBeeAttribute ), cv.Required(CONF_ATTRIBUTE_ID): cv.int_, cv.Required(CONF_TYPE): cv.enum( ATTR_TYPE, upper=True ), cv.Optional(CONF_ACCESS): cv.enum( ATTR_ACCESS, upper=True ), cv.Optional(CONF_VALUE): cv.valid, cv.Optional( CONF_REPORT, default=False ): cv.Any( cv.boolean, cv.one_of("force", lower=True), ), cv.Optional( CONF_ON_VALUE ): automation.validate_automation( { cv.GenerateID( CONF_TRIGGER_ID ): cv.declare_id( ZigBeeOnValueTrigger ), } ), cv.Optional(CONF_DEVICE): cv.use_id( cg.EntityBase ), cv.Optional( CONF_SCALE, default=1.0 ): cv.float_, cv.Optional( CONF_LAMBDA ): cv.returning_lambda, cv.Optional( CONF_MAX_LENGTH ): cv.int_range(0, 254), cv.Optional( CONF_ON_REPORT ): automation.validate_automation( { cv.GenerateID( CONF_TRIGGER_ID ): cv.declare_id( ZigBeeOnReportTrigger ), } ), } ), validate_attributes, ), }, ), validate_clusters, ), } ), ), cv.Optional(CONF_ON_JOIN): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), cv.require_framework_version(esp_idf=cv.Version(5, 1, 2)), _require_vfs_select, only_on_variant( supported=[ VARIANT_ESP32H2, VARIANT_ESP32C6, VARIANT_ESP32C5, ] ), ) def find_attr(conf, id): for ep in conf[CONF_ENDPOINTS]: for cl in ep.get(CONF_CLUSTERS, []): for attr in cl.get(CONF_ATTRIBUTES, []): if attr[CONF_ID] == id: return attr raise EsphomeError(f"Zigbee: Cannot find attribute {id}.") async def attributes_to_code(var, ep_num, cl): for attr in cl.get(CONF_ATTRIBUTES, []): if attr.get(CONF_ID) is None: cg.add( var.add_attr( ep_num, CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), CLUSTER_ROLE[cl[CONF_ROLE]], attr[CONF_ATTRIBUTE_ID], ATTR_TYPE[attr[CONF_TYPE]], attr[CONF_ACCESS], attr.get(CONF_MAX_LENGTH, 0), attr[CONF_VALUE], ) ) continue attr_var = cg.new_Pvariable( attr[CONF_ID], var, ep_num, CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), CLUSTER_ROLE[cl[CONF_ROLE]], attr[CONF_ATTRIBUTE_ID], ATTR_TYPE[attr[CONF_TYPE]], attr[CONF_SCALE], ) await cg.register_component(attr_var, attr) cg.add( attr_var.add_attr( attr[CONF_ACCESS], attr.get(CONF_MAX_LENGTH, 0), attr[CONF_VALUE], ) ) if attr[CONF_REPORT]: cg.add(attr_var.set_report(attr[CONF_REPORT] == "force")) if CONF_LAMBDA in attr: lambda_ = await cg.process_lambda( attr[CONF_LAMBDA], [(cg.float_, "x")], return_type=get_c_type(attr[CONF_TYPE]), ) if CONF_DEVICE in attr: device = await cg.get_variable(attr[CONF_DEVICE]) template_arg = cg.TemplateArguments(get_c_type(attr[CONF_TYPE])) if CONF_LAMBDA in attr: if device.base.type.inherits_from(Sensor): lambda_ = await cg.process_lambda( attr[CONF_LAMBDA], [(cg.float_, "x")], return_type=get_c_type(attr[CONF_TYPE]), ) elif device.base.type.inherits_from(BinarySensor): lambda_ = await cg.process_lambda( attr[CONF_LAMBDA], [(cg.bool_, "x")], return_type=get_c_type(attr[CONF_TYPE]), ) elif device.base.type.inherits_from(text_sensor.TextSensor): lambda_ = await cg.process_lambda( attr[CONF_LAMBDA], [(cg.std_string, "x")], return_type=get_c_type(attr[CONF_TYPE]), ) elif device.base.type.inherits_from(Switch): lambda_ = await cg.process_lambda( attr[CONF_LAMBDA], [(get_c_type(attr[CONF_TYPE]), "x")], return_type=cg.bool_, ) cg.add(attr_var.connect(template_arg, device, lambda_)) else: cg.add(attr_var.connect(template_arg, device)) for conf in attr.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], cg.TemplateArguments(get_c_type(attr[CONF_TYPE])), attr_var, ) await cg.register_component(trigger, conf) await automation.build_automation( trigger, [(get_c_type(attr[CONF_TYPE]), "x")], conf ) for conf in attr.get(CONF_ON_REPORT, []): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], cg.TemplateArguments(get_c_type(attr[CONF_TYPE])), attr_var, ) await cg.register_component(trigger, conf) value_type = get_c_type(attr[CONF_TYPE]) automation_arg_type = "esphome::zigbee::ZigBeeReportData" + str( cg.TemplateArguments(value_type) ) await automation.build_automation( trigger, [(cg.RawExpression(automation_arg_type), "x")], conf ) async def to_code(config): add_idf_component( name="espressif/esp-zboss-lib", ref="1.6.4", ) add_idf_component( name="espressif/esp-zigbee-lib", ref="1.6.8", ) add_idf_sdkconfig_option("CONFIG_ZB_ENABLED", True) if config.get(CONF_ROUTER): add_idf_sdkconfig_option("CONFIG_ZB_ZCZR", True) else: add_idf_sdkconfig_option("CONFIG_ZB_ZED", True) add_idf_sdkconfig_option("CONFIG_ZB_RADIO_NATIVE", True) # The pre-built Zigbee library uses esp_log_default_level which requires # dynamic log level control to be enabled add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", True) if CONF_WIFI in CORE.config: add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE", 4096) cg.add_define("CONFIG_WIFI_COEX") if config.get(CONF_DEBUG): add_idf_sdkconfig_option("CONFIG_ZB_DEBUG_MODE", True) # create endpoints ep_list, added_ids = create_ep(config, CORE.config) if added_ids: # update ESPHOME_COMPONENT_COUNT via pre build script cg.add_define("ZB_ESPHOME_COMPONENT_COUNT", comp_ids + added_ids) add_extra_script( "pre", "zigbee_pre_build.py", Path(__file__).parent / "zigbee_pre_build.py.script", ) # setup zigbee components var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) if CONF_TRUST_CENTER_KEY in config: cg.add(var.set_trust_center_key(config[CONF_TRUST_CENTER_KEY])) if CONF_DEVICE_VERSION in config: cg.add(var.set_device_version(config[CONF_DEVICE_VERSION])) if CONF_NAME not in config: config[CONF_NAME] = CORE.name or "" cg.add( var.set_basic_cluster( config[CONF_NAME], config[CONF_MANUFACTURER], config[CONF_DATE], config[CONF_POWER_SUPPLY], config[CONF_VERSION], 0, 0, CORE.area or "", config[CONF_AREA], ) ) for ep in ep_list: cg.add( var.create_default_cluster(ep[CONF_NUM], DEVICE_ID[ep[CONF_DEVICE_TYPE]]) ) for cl in ep.get(CONF_CLUSTERS, []): cg.add( var.add_cluster( ep[CONF_NUM], CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), CLUSTER_ROLE[cl[CONF_ROLE]], ) ) await attributes_to_code(var, ep[CONF_NUM], cl) await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) ZIGBEE_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(ZigBeeComponent), } ) @_register_action( "zigbee.reset", ResetZigbeeAction, automation.maybe_simple_id(ZIGBEE_ACTION_SCHEMA), synchronous=True, ) async def reset_zigbee_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 @_register_action( "zigbee.report", ReportAction, automation.maybe_simple_id(ZIGBEE_ACTION_SCHEMA), synchronous=True, ) async def report_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 ZIGBEE_ATTRIBUTE_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(ZigBeeAttribute), } ) ZIGBEE_SET_ATTR_SCHEMA = cv.All( automation.maybe_simple_id( ZIGBEE_ATTRIBUTE_ACTION_SCHEMA.extend( cv.Schema( { cv.Required(CONF_VALUE): cv.templatable(cv.valid), } ) ) ), ) @_register_action( "zigbee.setAttr", SetAttrAction, ZIGBEE_SET_ATTR_SCHEMA, synchronous=True ) async def zigbee_set_attr_to_code(config, action_id, template_arg, args): attr = find_attr( CORE.config["zigbee"], config[CONF_ID], ) template_arg = cg.TemplateArguments( get_c_type(attr[CONF_TYPE]), template_arg.args if template_arg.args.args else None, ) parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable( config[CONF_VALUE], args, get_c_type(attr[CONF_TYPE]) ) cg.add(var.set_value(template_)) return var @_register_action( "zigbee.reportAttr", ReportAttrAction, automation.maybe_simple_id(ZIGBEE_ATTRIBUTE_ACTION_SCHEMA), synchronous=True, ) async def zigbee_report_attr_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 ================================================ FILE: components/zigbee/automation.cpp ================================================ #include "automation.h" #include #include "esphome/core/log.h" namespace esphome { namespace zigbee { /** * @brief sRGB to R'G'B' gamma correction as outlined in * https://en.wikipedia.org/wiki/SRGB * * @param linear a component value in sRGB color space * @return float the corresponding component value in R'G'B' color space */ inline float gamma_correct(float linear) { if(linear < 0.0031308f) { return linear * 12.92f; } else { return (1.055f) * pow(linear, (1.0f / 2.4f)) - 0.055f; } } /* The following conversions operate on xyY with Y = 1.0, hence Y doesn't appear in the formulas. The coefficients originate from https://en.wikipedia.org/wiki/SRGB#Primaries */ float get_r_from_xy(float x, float y) { float z = 1.0f - x - y; float X = x / y; float Z = z / y; float r = X * 3.2406 - 1.5372f - Z * 0.4986f; return std::clamp(gamma_correct(r), 0.0f, 1.0f); } float get_g_from_xy(float x, float y) { float z = 1.0f - x - y; float X = x / y; float Z = z / y; float g = -X * 0.9689f + 1.8758f + Z * 0.0415f; return std::clamp(gamma_correct(g), 0.0f, 1.0f); } float get_b_from_xy(float x, float y) { float z = 1.0f - x - y; float X = x / y; float Z = z / y; float b = X * 0.0557f - 0.2040f + Z * 1.0570f; return std::clamp(gamma_correct(b), 0.0f, 1.0f); } #ifdef USE_LIGHT void set_light_color(uint8_t ep, light::LightCall *call, uint16_t value, bool is_x) { static std::map x; static std::map y; if (is_x) { x[ep] = (float) value / 65536; } else { y[ep] = (float) value / 65536; } ESP_LOGD(TAG, "Set color, x: %f, y: %f", x[ep], y[ep]); call->set_rgb(get_r_from_xy(x[ep], y[ep]), get_g_from_xy(x[ep], y[ep]), get_b_from_xy(x[ep], y[ep])); } #endif } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/automation.h ================================================ #pragma once //#include //deactive because not working with esp-idf 5.1.4 #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/version.h" #include "esphome/core/log.h" #include "zigbee_attribute.h" #include "zigbee.h" #ifdef USE_LIGHT #include "esphome/components/light/light_state.h" #endif namespace esphome { namespace zigbee { template class ResetZigbeeAction : public Action, public Parented { public: #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) void play(const Ts &...x) override { this->parent_->reset(); } #else void play(Ts... x) override { this->parent_->reset(); } #endif }; template class ReportAction : public Action, public Parented { public: #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) void play(const Ts &...x) override { this->parent_->report(); } #else void play(Ts... x) override { this->parent_->report(); } #endif }; template class ReportAttrAction : public Action, public Parented { public: #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) void play(const Ts &...x) override { this->parent_->report(); } #else void play(Ts... x) override { this->parent_->report(); } #endif }; template class SetAttrAction : public Action { public: SetAttrAction(ZigBeeAttribute *parent) : parent_(parent) {} TEMPLATABLE_VALUE(T, value); #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0) void play(const Ts &...x) override { this->parent_->set_attr(this->value_.value(x...)); } #else void play(Ts... x) override { this->parent_->set_attr(this->value_.value(x...)); } #endif protected: ZigBeeAttribute *parent_; }; template class ZigBeeOnValueTrigger : public Trigger, public Component { public: explicit ZigBeeOnValueTrigger(ZigBeeAttribute *parent) : parent_(parent) {} void setup() override { this->parent_->add_on_value_callback([this](esp_zb_zcl_attribute_t attribute) { this->on_value_(attribute); }); } protected: void on_value_(esp_zb_zcl_attribute_t attribute) { if (attribute.data.type == parent_->attr_type() && attribute.data.value) { this->trigger(get_value_by_type(parent_->attr_type(), attribute.data.value)); } } ZigBeeAttribute *parent_; }; template struct ZigBeeReportData { T value; esp_zb_zcl_addr_t src_address; uint8_t src_endpoint; }; template class ZigBeeOnReportTrigger : public Trigger>, public Component { public: explicit ZigBeeOnReportTrigger(ZigBeeAttribute *parent) : parent_(parent) {} void setup() override { this->parent_->add_on_report_callback( [this](esp_zb_zcl_attribute_t attribute, esp_zb_zcl_addr_t src_address, uint8_t src_endpoint) { this->on_report_(attribute, src_address, src_endpoint); }); } protected: void on_report_(esp_zb_zcl_attribute_t attribute, esp_zb_zcl_addr_t src_address, uint8_t src_endpoint) { if (attribute.data.type == parent_->attr_type() && attribute.data.value) { this->trigger(ZigBeeReportData{ .value = get_value_by_type(parent_->attr_type(), attribute.data.value), .src_address = src_address, .src_endpoint = src_endpoint, }); } } ZigBeeAttribute *parent_; }; template T get_value_by_type(uint8_t attr_type, void *data) { switch (attr_type) { case ESP_ZB_ZCL_ATTR_TYPE_SEMI: return 0; //(T) * (std::float16_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_OCTET_STRING: return 0; case ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING: return 0; case ESP_ZB_ZCL_ATTR_TYPE_LONG_OCTET_STRING: return 0; case ESP_ZB_ZCL_ATTR_TYPE_LONG_CHAR_STRING: return 0; case ESP_ZB_ZCL_ATTR_TYPE_ARRAY: return 0; case ESP_ZB_ZCL_ATTR_TYPE_16BIT_ARRAY: return 0; case ESP_ZB_ZCL_ATTR_TYPE_32BIT_ARRAY: return 0; case ESP_ZB_ZCL_ATTR_TYPE_STRUCTURE: return 0; case ESP_ZB_ZCL_ATTR_TYPE_SET: return 0; case ESP_ZB_ZCL_ATTR_TYPE_BAG: return 0; case ESP_ZB_ZCL_ATTR_TYPE_TIME_OF_DAY: return (T) * (uint32_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_DATE: return (T) * (uint32_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_UTC_TIME: return (T) * (uint32_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_CLUSTER_ID: return (T) * (uint16_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_ATTRIBUTE_ID: return (T) * (uint16_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_BACNET_OID: return 0; case ESP_ZB_ZCL_ATTR_TYPE_IEEE_ADDR: return (T) * (uint64_t *) data; case ESP_ZB_ZCL_ATTR_TYPE_128_BIT_KEY: return 0; default: return *(T *) data; } } float get_r_from_xy(float x, float y); float get_g_from_xy(float x, float y); float get_b_from_xy(float x, float y); #ifdef USE_LIGHT void set_light_color(uint8_t ep, light::LightCall *call, uint16_t value, bool is_x); #endif } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/const.py ================================================ from dataclasses import dataclass import esphome.codegen as cg CONF_ENDPOINTS = "endpoints" CONF_DEVICE_TYPE = "device_type" CONF_NUM = "num" CONF_CLUSTERS = "clusters" CONF_ON_JOIN = "on_join" CONF_MANUFACTURER = "manufacturer" CONF_ATTRIBUTES = "attributes" CONF_ROLE = "role" CONF_ENDPOINT = "endpoint" CONF_CLUSTER = "cluster" CONF_REPORT = "report" CONF_ACCESS = "access" CONF_SCALE = "scale" CONF_ATTRIBUTE_ID = "attribute_id" CONF_ZIGBEE_ID = "zigbee_id" CONF_ROUTER = "router" CONF_AS_GENERIC = "as_generic" CONF_ON_REPORT = "on_report" CONF_TRUST_CENTER_KEY = "trust_center_key" CONF_DEVICE_VERSION = "device_version" # dummies for upstream compatibility binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) sensor_ns = cg.esphome_ns.namespace("sensor") Sensor = sensor_ns.class_("Sensor", cg.EntityBase) switch_ns = cg.esphome_ns.namespace("switch_") # NB: "switch_" not "switch" Switch = switch_ns.class_("Switch", cg.EntityBase) @dataclass class AnalogInputType: Temp_Degrees_C = 0x00 Relative_Humidity_Percent = 0x01 Pressure_Pascal = 0x02 Flow_Liters_Per_Sec = 0x03 Percentage = 0x04 Parts_Per_Million = 0x05 Rotational_Speed_RPM = 0x06 Current_Amps = 0x07 Frequency_Hz = 0x08 Power_Watts = 0x09 Power_Kilo_Watts = 0x0A Energy_Kilo_Watt_Hours = 0x0B Count = 0x0C Enthalpy_KJoules_Per_Kg = 0x0D Time_Seconds = 0x0E @dataclass class BacnetUnit: """BACnet units.""" SQUARE_METERS = 0 SQUARE_FEET = 1 MILLIAMPERES = 2 AMPERES = 3 OHMS = 4 VOLTS = 5 KILOVOLTS = 6 MEGAVOLTS = 7 VOLT_AMPERES = 8 KILOVOLT_AMPERES = 9 MEGAVOLT_AMPERES = 10 VOLT_AMPERES_REACTIVE = 11 KILOVOLT_AMPERES_REACTIVE = 12 MEGAVOLT_AMPERES_REACTIVE = 13 DEGREES_PHASE = 14 POWER_FACTOR = 15 JOULES = 16 KILOJOULES = 17 WATT_HOURS = 18 KILOWATT_HOURS = 19 BTUS = 20 THERMS = 21 TON_HOURS = 22 JOULES_PER_KILOGRAM_DRY_AIR = 23 BTUS_PER_POUND_DRY_AIR = 24 CYCLES_PER_HOUR = 25 CYCLES_PER_MINUTE = 26 HERTZ = 27 GRAMS_OF_WATER_PER_KILOGRAM_DRY_AIR = 28 PERCENT_RELATIVE_HUMIDITY = 29 MILLIMETERS = 30 METERS = 31 INCHES = 32 FEET = 33 WATTS_PER_SQUARE_FOOT = 34 WATTS_PER_SQUARE_METER = 35 LUMENS = 36 LUXES = 37 FOOT_CANDLES = 38 KILOGRAMS = 39 POUNDS_MASS = 40 TONS = 41 KILOGRAMS_PER_SECOND = 42 KILOGRAMS_PER_MINUTE = 43 KILOGRAMS_PER_HOUR = 44 POUNDS_MASS_PER_MINUTE = 45 POUNDS_MASS_PER_HOUR = 46 WATTS = 47 KILOWATTS = 48 MEGAWATTS = 49 BTUS_PER_HOUR = 50 HORSEPOWER = 51 TONS_REFRIGERATION = 52 PASCALS = 53 KILOPASCALS = 54 BARS = 55 POUNDS_FORCE_PER_SQUARE_INCH = 56 CENTIMETERS_OF_WATER = 57 INCHES_OF_WATER = 58 MILLIMETERS_OF_MERCURY = 59 CENTIMETERS_OF_MERCURY = 60 INCHES_OF_MERCURY = 61 DEGREES_CELSIUS = 62 KELVIN = 63 DEGREES_FAHRENHEIT = 64 DEGREE_DAYS_CELSIUS = 65 DEGREE_DAYS_FAHRENHEIT = 66 YEARS = 67 MONTHS = 68 WEEKS = 69 DAYS = 70 HOURS = 71 MINUTES = 72 SECONDS = 73 METERS_PER_SECOND = 74 KILOMETERS_PER_HOUR = 75 FEET_PER_SECOND = 76 FEET_PER_MINUTE = 77 MILES_PER_HOUR = 78 CUBIC_FEET = 79 CUBIC_METERS = 80 IMPERIAL_GALLONS = 81 LITERS = 82 US_GALLONS = 83 CUBIC_FEET_PER_MINUTE = 84 CUBIC_METERS_PER_SECOND = 85 IMPERIAL_GALLONS_PER_MINUTE = 86 LITERS_PER_SECOND = 87 LITERS_PER_MINUTE = 88 US_GALLONS_PER_MINUTE = 89 DEGREES_ANGULAR = 90 DEGREES_CELSIUS_PER_HOUR = 91 DEGREES_CELSIUS_PER_MINUTE = 92 DEGREES_FAHRENHEIT_PER_HOUR = 93 DEGREES_FAHRENHEIT_PER_MINUTE = 94 NO_UNITS = 95 PARTS_PER_MILLION = 96 PARTS_PER_BILLION = 97 PERCENT = 98 PERCENT_PER_SECOND = 99 PER_MINUTE = 100 PER_SECOND = 101 PSI_PER_DEGREE_FAHRENHEIT = 102 RADIANS = 103 REVOLUTIONS_PER_MINUTE = 104 CURRENCY1 = 105 CURRENCY2 = 106 CURRENCY3 = 107 CURRENCY4 = 108 CURRENCY5 = 109 CURRENCY6 = 110 CURRENCY7 = 111 CURRENCY8 = 112 CURRENCY9 = 113 CURRENCY10 = 114 SQUARE_INCHES = 115 SQUARE_CENTIMETERS = 116 BTUS_PER_POUND = 117 CENTIMETERS = 118 POUNDS_MASS_PER_SECOND = 119 DELTA_DEGREES_FAHRENHEIT = 120 DELTA_KELVIN = 121 KILOHMS = 122 MEGOHMS = 123 MILLIVOLTS = 124 KILOJOULES_PER_KILOGRAM = 125 MEGAJOULES = 126 JOULES_PER_DEGREE_KELVIN = 127 JOULES_PER_KILOGRAM_DEGREE_KELVIN = 128 KILOHERTZ = 129 MEGAHERTZ = 130 PER_HOUR = 131 MILLIWATTS = 132 HECTOPASCALS = 133 MILLIBARS = 134 CUBIC_METERS_PER_HOUR = 135 LITERS_PER_HOUR = 136 KW_HOURS_PER_SQUARE_METER = 137 KW_HOURS_PER_SQUARE_FOOT = 138 MEGAJOULES_PER_SQUARE_METER = 139 MEGAJOULES_PER_SQUARE_FOOT = 140 WATTS_PER_SQUARE_METER_DEGREE_KELVIN = 141 CUBIC_FEET_PER_SECOND = 142 PERCENT_OBSCURATION_PER_FOOT = 143 PERCENT_OBSCURATION_PER_METER = 144 MILLIOHMS = 145 MEGAWATT_HOURS = 146 KILO_BTUS = 147 MEGA_BTUS = 148 KILOJOULES_PER_KILOGRAM_DRY_AIR = 149 MEGAJOULES_PER_KILOGRAM_DRY_AIR = 150 KILOJOULES_PER_DEGREE_KELVIN = 151 MEGAJOULES_PER_DEGREE_KELVIN = 152 NEWTON = 153 GRAMS_PER_SECOND = 154 GRAMS_PER_MINUTE = 155 TONS_PER_HOUR = 156 KILO_BTUS_PER_HOUR = 157 HUNDREDTHS_SECONDS = 158 MILLISECONDS = 159 NEWTON_METERS = 160 MILLIMETERS_PER_SECOND = 161 MILLIMETERS_PER_MINUTE = 162 METERS_PER_MINUTE = 163 METERS_PER_HOUR = 164 CUBIC_METERS_PER_MINUTE = 165 METERS_PER_SECOND_PER_SECOND = 166 AMPERES_PER_METER = 167 AMPERES_PER_SQUARE_METER = 168 AMPERE_SQUARE_METERS = 169 FARADS = 170 HENRYS = 171 OHM_METERS = 172 SIEMENS_PER_METER = 174 TESLAS = 175 VOLTS_PER_DEGREE_KELVIN = 176 VOLTS_PER_METER = 177 WEBERS = 178 CANDELAS = 179 CANDELAS_PER_SQUARE_METER = 180 KELVIN_PER_HOUR = 181 KELVIN_PER_MINUTE = 182 JOULE_SECONDS = 183 RADIANS_PER_SECOND = 184 SQUARE_METERS_PER_NEWTON = 185 KILOGRAMS_PER_CUBIC_METER = 186 NEWTON_SECONDS = 187 NEWTONS_PER_METER = 188 WATTS_PER_METER_PER_DEGREE_KELVIN = 189 MICROSIEMENS = 190 CUBIC_FEET_PER_HOUR = 191 US_GALLONS_PER_HOUR = 192 KILOMETERS = 193 MICROMETERS = 194 GRAMS = 195 MILLIGRAMS = 196 MILLILITERS = 197 MILLILITERS_PER_SECOND = 198 DECIBELS = 199 DECIBELS_MILLIVOLT = 200 DECIBELS_VOLT = 201 MILLISIEMENS = 202 WATT_REACTIVE_HOURS = 203 KILOWATT_REACTIVE_HOURS = 204 MEGAWATT_REACTIVE_HOURS = 205 MILLIMETERS_OF_WATER = 206 PER_MILLE = 207 GRAMS_PER_GRAM = 208 KILOGRAMS_PER_KILOGRAM = 209 GRAMS_PER_KILOGRAM = 210 MILLIGRAMS_PER_GRAM = 211 MILLIGRAMS_PER_KILOGRAM = 212 GRAMS_PER_MILLILITER = 213 GRAMS_PER_LITER = 214 MILLIGRAMS_PER_LITER = 215 MICROGRAMS_PER_LITER = 216 GRAMS_PER_CUBIC_METER = 217 MILLIGRAMS_PER_CUBIC_METER = 218 MICROGRAMS_PER_CUBIC_METER = 219 NANOGRAMS_PER_CUBIC_METER = 220 GRAMS_PER_CUBIC_CENTIMETER = 221 BECQUERELS = 222 KILOBECQUERELS = 223 MEGABECQUERELS = 224 GRAY = 225 MILLIGRAY = 226 MICROGRAY = 227 SIEVERTS = 228 MILLISIEVERTS = 229 MICROSIEVERTS = 230 MICROSIEVERTS_PER_HOUR = 231 DECIBELS_A = 232 NEPHELOMETRIC_TURBIDITY_UNIT = 233 PH = 234 GRAMS_PER_SQUARE_METER = 235 MINUTES_PER_DEGREE_KELVIN = 236 OHM_METER_SQUARED_PER_METER = 237 AMPERE_SECONDS = 238 VOLT_AMPERE_HOURS = 239 KILOVOLT_AMPERE_HOURS = 240 MEGAVOLT_AMPERE_HOURS = 241 VOLT_AMPERE_REACTIVE_HOURS = 242 KILOVOLT_AMPERE_REACTIVE_HOURS = 243 MEGAVOLT_AMPERE_REACTIVE_HOURS = 244 VOLT_SQUARE_HOURS = 245 AMPERE_SQUARE_HOURS = 246 JOULE_PER_HOURS = 247 CUBIC_FEET_PER_DAY = 248 CUBIC_METERS_PER_DAY = 249 WATT_HOURS_PER_CUBIC_METER = 250 JOULES_PER_CUBIC_METER = 251 MOLE_PERCENT = 252 PASCAL_SECONDS = 253 MILLION_STANDARD_CUBIC_FEET_PER_MINUTE = 254 ================================================ FILE: components/zigbee/esp_zb_event.h ================================================ #pragma once #ifdef USE_ESP32 #include // for offsetof #include // for memcpy #include "esp_zigbee_core.h" #include "zboss_api.h" #include "ha/esp_zigbee_ha_standard.h" namespace esphome::zigbee { class ZBEvent { public: ZBEvent(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level) { this->callback_id_ = ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID; this->init_set_attr_value_data(info, attribute, current_level); } ZBEvent(const esp_zb_zcl_report_attr_message_t *message) { this->callback_id_ = ESP_ZB_CORE_REPORT_ATTR_CB_ID; this->init_report_attr_data(message); } ZBEvent(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables) { this->callback_id_ = ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID; this->init_read_attr_resp_data(info, variables); } ~ZBEvent() { this->release(); } ZBEvent() : event_{}, callback_id_(ESP_ZB_CORE_BASIC_RESET_TO_FACTORY_RESET_CB_ID) {} void release() { // Free any allocated memory within the event switch (this->callback_id_) { case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID: if (this->event_.set_attr.attribute.data.value != nullptr && this->event_.set_attr.attribute.data.value != this->event_.set_attr.inline_data) { free(this->event_.set_attr.attribute.data.value); this->event_.set_attr.attribute.data.value = nullptr; } break; case ESP_ZB_CORE_REPORT_ATTR_CB_ID: if (this->event_.report_attr.attribute.data.value != nullptr && this->event_.report_attr.attribute.data.value != this->event_.report_attr.inline_data) { free(this->event_.report_attr.attribute.data.value); this->event_.report_attr.attribute.data.value = nullptr; } break; case ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID: { esp_zb_zcl_read_attr_resp_variable_t *var = &(this->event_.read_attr_resp.variables); while (var != nullptr) { if (var->attribute.data.value != nullptr && var->attribute.data.value != this->event_.read_attr_resp.inline_data) { free(var->attribute.data.value); var->attribute.data.value = nullptr; } var = var->next; } } // Reset the event data this->callback_id_ = ESP_ZB_CORE_BASIC_RESET_TO_FACTORY_RESET_CB_ID; // or some invalid value break; default: break; } } void load_set_attr_value_event(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level) { this->release(); this->callback_id_ = ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID; this->init_set_attr_value_data(info, attribute, current_level); } void load_report_attr_event(const esp_zb_zcl_report_attr_message_t *message) { this->release(); this->callback_id_ = ESP_ZB_CORE_REPORT_ATTR_CB_ID; this->init_report_attr_data(message); } void load_read_attr_resp_event(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables) { this->release(); this->callback_id_ = ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID; this->init_read_attr_resp_data(info, variables); } // Disable copy to prevent double-delete ZBEvent(const ZBEvent &) = delete; ZBEvent &operator=(const ZBEvent &) = delete; union { struct set_attr_event { esp_zb_device_cb_common_info_t info; esp_zb_zcl_attribute_t attribute; uint8_t current_level; bool has_current_level; uint8_t inline_data[4]; // For small data types } set_attr; struct report_attr_event { uint8_t dst_endpoint; uint16_t cluster; esp_zb_zcl_attribute_t attribute; esp_zb_zcl_addr_t src_address; uint8_t src_endpoint; uint8_t inline_data[4]; // For small data types } report_attr; struct read_attr_resp_event { esp_zb_zcl_cmd_info_t info; esp_zb_zcl_read_attr_resp_variable_t variables; uint8_t inline_data[4]; // For small data types } read_attr_resp; } event_; esp_zb_core_action_callback_id_t callback_id_; private: void init_set_attr_value_data(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level) { this->event_.set_attr.info = info; this->event_.set_attr.attribute = attribute; // get attribute.data.value with correct type this->event_.set_attr.has_current_level = (current_level != nullptr); if (current_level != nullptr) { this->event_.set_attr.current_level = *current_level; } if (attribute.data.value != nullptr) { // Copy the attribute value to avoid dangling pointer issues size_t value_size = this->get_attribute_value_size_(attribute.data); if (value_size > 4) { this->event_.set_attr.attribute.data.value = malloc(value_size); if (this->event_.set_attr.attribute.data.value != nullptr) { memcpy(this->event_.set_attr.attribute.data.value, attribute.data.value, value_size); } } else { memcpy(this->event_.set_attr.inline_data, attribute.data.value, value_size); this->event_.set_attr.attribute.data.value = this->event_.set_attr.inline_data; } } } void init_report_attr_data(const esp_zb_zcl_report_attr_message_t *message) { this->event_.report_attr.dst_endpoint = message->dst_endpoint; this->event_.report_attr.cluster = message->cluster; this->event_.report_attr.attribute = message->attribute; this->event_.report_attr.src_address = message->src_address; this->event_.report_attr.src_endpoint = message->src_endpoint; if (message->attribute.data.value != nullptr) { // Copy the attribute value to avoid dangling pointer issues size_t value_size = this->get_attribute_value_size_(message->attribute.data); if (value_size > 4) { this->event_.report_attr.attribute.data.value = malloc(value_size); if (this->event_.report_attr.attribute.data.value != nullptr) { memcpy(this->event_.report_attr.attribute.data.value, message->attribute.data.value, value_size); } } else { memcpy(this->event_.report_attr.inline_data, message->attribute.data.value, value_size); this->event_.report_attr.attribute.data.value = this->event_.report_attr.inline_data; } } } void init_read_attr_resp_data(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables) { this->event_.read_attr_resp.info = info; if (variables == nullptr) { return; } this->event_.read_attr_resp.variables.status = variables->status; this->event_.read_attr_resp.variables.attribute.id = variables->attribute.id; this->event_.read_attr_resp.variables.attribute.data.type = variables->attribute.data.type; this->event_.read_attr_resp.variables.attribute.data.size = variables->attribute.data.size; // Note: variables is a pointer to a struct/list; deep copy needed if (variables->attribute.data.value != nullptr) { size_t value_size = this->get_attribute_value_size_(variables->attribute.data); if (value_size > 4) { this->event_.read_attr_resp.variables.attribute.data.value = malloc(value_size); if (this->event_.read_attr_resp.variables.attribute.data.value != nullptr) { memcpy(this->event_.read_attr_resp.variables.attribute.data.value, variables->attribute.data.value, value_size); } } else { memcpy(this->event_.read_attr_resp.inline_data, variables->attribute.data.value, value_size); this->event_.read_attr_resp.variables.attribute.data.value = this->event_.read_attr_resp.inline_data; } } esp_zb_zcl_read_attr_resp_variable_t *var = variables; esp_zb_zcl_read_attr_resp_variable_t *var_last = &(this->event_.read_attr_resp.variables); while (var->next != nullptr) { var = var->next; size_t value_size = this->get_attribute_value_size_(var->attribute.data); var_last->next = new esp_zb_zcl_read_attr_resp_variable_t(); if (var_last->next != nullptr) { var_last->next->attribute.id = var->attribute.id; var_last->next->status = var->status; var_last->next->attribute.data.type = var->attribute.data.type; var_last->next->attribute.data.size = var->attribute.data.size; var_last->next->attribute.data.value = malloc(value_size); if (var_last->next->attribute.data.value != nullptr) { memcpy(var_last->next->attribute.data.value, var->attribute.data.value, value_size); } var_last = var_last->next; } } } size_t get_attribute_value_size_(esp_zb_zcl_attribute_data_t data) { // Determine attribute value size based on attribute type (and size) uint8_t len = 0; switch (data.type) { case ESP_ZB_ZCL_ATTR_TYPE_BOOL: return sizeof(bool); case ESP_ZB_ZCL_ATTR_TYPE_U8: return sizeof(uint8_t); case ESP_ZB_ZCL_ATTR_TYPE_U16: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_U24: return 3; // 24 bits = 3 bytes case ESP_ZB_ZCL_ATTR_TYPE_U32: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_U40: return 5; // 40 bits = 5 bytes case ESP_ZB_ZCL_ATTR_TYPE_U48: return 6; // 48 bits = 6 bytes case ESP_ZB_ZCL_ATTR_TYPE_U56: return 7; // 56 bits = 7 bytes case ESP_ZB_ZCL_ATTR_TYPE_U64: return sizeof(uint64_t); case ESP_ZB_ZCL_ATTR_TYPE_S8: return sizeof(int8_t); case ESP_ZB_ZCL_ATTR_TYPE_S16: return sizeof(int16_t); case ESP_ZB_ZCL_ATTR_TYPE_S24: return 3; // 24 bits = 3 bytes case ESP_ZB_ZCL_ATTR_TYPE_S32: return sizeof(int32_t); case ESP_ZB_ZCL_ATTR_TYPE_S40: return 5; // 40 bits = 5 bytes case ESP_ZB_ZCL_ATTR_TYPE_S48: return 6; // 48 bits = 6 bytes case ESP_ZB_ZCL_ATTR_TYPE_S56: return 7; // 56 bits = 7 bytes case ESP_ZB_ZCL_ATTR_TYPE_S64: return sizeof(int64_t); case ESP_ZB_ZCL_ATTR_TYPE_8BITMAP: return sizeof(uint8_t); case ESP_ZB_ZCL_ATTR_TYPE_16BITMAP: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_24BITMAP: return 3; // 24 bits = 3 bytes case ESP_ZB_ZCL_ATTR_TYPE_32BITMAP: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_40BITMAP: return 5; // 40 bits = 5 bytes case ESP_ZB_ZCL_ATTR_TYPE_48BITMAP: return 6; // 48 bits = 6 bytes case ESP_ZB_ZCL_ATTR_TYPE_56BITMAP: return 7; // 56 bits = 7 bytes case ESP_ZB_ZCL_ATTR_TYPE_64BITMAP: return sizeof(uint64_t); case ESP_ZB_ZCL_ATTR_TYPE_8BIT: return sizeof(uint8_t); case ESP_ZB_ZCL_ATTR_TYPE_16BIT: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_24BIT: return 3; // 24 bits = 3 bytes case ESP_ZB_ZCL_ATTR_TYPE_32BIT: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_40BIT: return 5; // 40 bits = 5 bytes case ESP_ZB_ZCL_ATTR_TYPE_48BIT: return 6; // 48 bits = 6 bytes case ESP_ZB_ZCL_ATTR_TYPE_56BIT: return 7; // 56 bits = 7 bytes case ESP_ZB_ZCL_ATTR_TYPE_64BIT: return sizeof(uint64_t); case ESP_ZB_ZCL_ATTR_TYPE_8BIT_ENUM: return sizeof(uint8_t); case ESP_ZB_ZCL_ATTR_TYPE_16BIT_ENUM: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_SEMI: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_SINGLE: return sizeof(float); case ESP_ZB_ZCL_ATTR_TYPE_DOUBLE: return sizeof(double); case ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING: len = *((uint8_t *) data.value); // first byte is length return len + 1; // length byte + string case ESP_ZB_ZCL_ATTR_TYPE_TIME_OF_DAY: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_DATE: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_UTC_TIME: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_CLUSTER_ID: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_ATTRIBUTE_ID: return sizeof(uint16_t); case ESP_ZB_ZCL_ATTR_TYPE_BACNET_OID: return sizeof(uint32_t); case ESP_ZB_ZCL_ATTR_TYPE_IEEE_ADDR: return sizeof(uint64_t); case ESP_ZB_ZCL_ATTR_TYPE_128_BIT_KEY: return 16; // 128 bits = 16 bytes default: return 0; // Unknown type } } }; } // namespace esphome::zigbee #endif // USE_ESP32 ================================================ FILE: components/zigbee/files_to_parse/parse_zigbee_headers.py ================================================ #!/usr/bin/env python3 """ Create python enums """ from pycparser import c_ast, parse_file filename = "esp_zigbee_zcl_common.h" ast = parse_file(filename, use_cpp=True) typedefs = [t[1].type for t in ast.children() if isinstance(t[1], c_ast.Typedef)] my_enums = [t for t in typedefs if isinstance(t.type, c_ast.Enum)] def print_enum(enum): print(enum.declname + ":") print( "\n".join([e.name + ": " + e.value.value for e in enum.type.values.enumerators]) ) def write_profileIDs(enums): enum = list(filter(lambda e: e.declname == "esp_zb_ha_standard_devices_t", enums))[ 0 ] enum_name = "ha_standard_devices" to_py = f'{enum_name} = cg.esphome_ns.enum("{enum.declname}")\nDEVICE_ID' + " = {\n" for e in enum.type.values.enumerators: to_py += f' "{e.name.removeprefix("ESP_ZB_HA_").removesuffix("_DEVICE_ID")}": {enum_name}.{e.name},\n' to_py += f' {(e.value.value.removesuffix("U").upper().replace("X","x"))}: {enum_name}.{e.name},\n' to_py += "}\n" return to_py def write_clusterIDs(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_cluster_id_t", enums))[0] enum_name = "cluster_id" to_py = ( f'{enum_name} = cg.esphome_ns.enum("{enum.declname}")\nCLUSTER_ID' + " = {\n" ) for e in enum.type.values.enumerators: to_py += f' "{e.name.removeprefix("ESP_ZB_ZCL_CLUSTER_ID_")}": {enum_name}.{e.name},\n' to_py += f' {(e.value.value.removesuffix("U").upper().replace("X","x"))}: {enum_name}.{e.name},\n' to_py += "}\n" return to_py def write_clusterRoles(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_cluster_role_t", enums))[0] enum_name = "cluster_role" to_py = ( f'{enum_name} = cg.esphome_ns.enum("{enum.declname}")\nCLUSTER_ROLE' + " = {\n" ) for e in enum.type.values.enumerators: to_py += f' "{e.name.removeprefix("ESP_ZB_ZCL_CLUSTER_").removesuffix("_ROLE")}": {enum_name}.{e.name},\n' to_py += "}\n" return to_py def write_ZBtypes(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_attr_type_t", enums))[0] enum_name = "attr_type" to_py = f'{enum_name} = cg.esphome_ns.enum("{enum.declname}")\nATTR_TYPE' + " = {\n" for e in enum.type.values.enumerators: to_py += f' "{e.name.removeprefix("ESP_ZB_ZCL_ATTR_TYPE_")}": {enum_name}.{e.name},\n' to_py += "}\n" return to_py with open("zigbee_const.py", "w", encoding="utf-8") as f: f.write("import esphome.codegen as cg\n\n") f.write(write_profileIDs(my_enums)) f.write(write_clusterIDs(my_enums)) f.write(write_clusterRoles(my_enums)) f.write(write_ZBtypes(my_enums)) f.write( """ATTR_ACCESS = { "READ_ONLY": 1, "WRITE_ONLY": 2, "READ_WRITE": 3, } """ ) # [print_enum(e) for e in enums] replacements_cl = { "level_control": "level", "multi_value": "multistate_value", "multi_input": "multistate_input", "multi_output": "multistate_output", "ota_upgrade": "ota", "illuminance_measurement": "illuminance_meas", "temp_measurement": "temperature_meas", "pressure_measurement": "pressure_meas", "rel_humidity_measurement": "humidity_meas", "electrical_measurement": "electrical_meas", "flow_measurement": "flow_meas", } remove_cl = [ "rssi_location", "green_power", "keep_alive", "pump_config_control", "ballast_config", ] def write_cluster_list_aou(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_cluster_id_t", enums))[0] ids = [e.name for e in enum.type.values.enumerators] to_py = "esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask) {\n esp_err_t ret;\n" to_py += " ret = esp_zb_cluster_list_update_cluster(cluster_list, attr_list, cluster_id, role_mask);\n" to_py += " if (ret != ESP_OK) {\n" to_py += ( ' ESP_LOGE("zigbee_helper", "Ignore previous cluster not found error");\n' ) to_py += " switch (cluster_id) {\n" for id in ids: cluster_name = id.removeprefix("ESP_ZB_ZCL_CLUSTER_ID_").lower() if cluster_name in remove_cl: continue for k, i in replacements_cl.items(): cluster_name = cluster_name.replace(k, i) to_py += f" case {id}:\n" to_py += f" ret = esp_zb_cluster_list_add_{cluster_name}_cluster(cluster_list, attr_list, role_mask);\n" to_py += " break;\n" to_py += " default:\n ret = esp_zb_cluster_list_add_custom_cluster(cluster_list, attr_list, role_mask);\n }\n }\n return ret;\n}\n\n" return to_py replacements_attrl = { "level_control": "level", "multi_value": "multistate_value", "multi_input": "multistate_input", "multi_output": "multistate_output", "ota_upgrade": "ota", "illuminance_measurement": "illuminance_meas", "temp_measurement": "temperature_meas", "pressure_measurement": "pressure_meas", "rel_humidity_measurement": "humidity_meas", "electrical_measurement": "electrical_meas", "flow_measurement": "flow_meas", } void_list = [] def write_attr_list_create(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_cluster_id_t", enums))[0] ids = [e.name for e in enum.type.values.enumerators] to_py = "esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id) {\n" to_py += " switch (cluster_id) {\n" for id in ids: cluster_name = id.removeprefix("ESP_ZB_ZCL_CLUSTER_ID_").lower() if cluster_name in remove_cl: continue for k, i in replacements_attrl.items(): cluster_name = cluster_name.replace(k, i) to_py += f" case {id}:\n" if cluster_name in void_list: to_py += f" return esp_zb_{cluster_name}_cluster_create();\n" else: to_py += f" return esp_zb_{cluster_name}_cluster_create(NULL);\n" to_py += " default:\n return esp_zb_zcl_attr_list_create(cluster_id);\n }\n}\n\n" return to_py remove_attra = [ "rssi_location", "green_power", "keep_alive", "pump_config_control", "ballast_config", "ias_ace", "price", "metering", ] def write_attr_add(enums): enum = list(filter(lambda e: e.declname == "esp_zb_zcl_cluster_id_t", enums))[0] ids = [e.name for e in enum.type.values.enumerators] to_py = "esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p) {\n" to_py += " switch (cluster_id) {\n" for id in ids: cluster_name = id.removeprefix("ESP_ZB_ZCL_CLUSTER_ID_").lower() if cluster_name in remove_attra: continue for k, i in replacements_cl.items(): cluster_name = cluster_name.replace(k, i) to_py += f" case {id}:\n" to_py += f" return esp_zb_{cluster_name}_cluster_add_attr(attr_list, attr_id, value_p);\n" to_py += " default:\n return ESP_FAIL;\n }\n}\n\n" return to_py with open("c_helpers.c", "w", encoding="utf-8") as f: f.write(write_cluster_list_aou(my_enums)) f.write(write_attr_list_create(my_enums)) f.write(write_attr_add(my_enums)) ================================================ FILE: components/zigbee/partitions_zb.csv ================================================ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, app0, app, ota_0, , 0x1B0000, app1, app, ota_1, , 0x1B0000, nvs, data, nvs, , 0x6D000, zb_storage, data, fat, , 16K, zb_fct, data, fat, , 1K, ================================================ FILE: components/zigbee/time/__init__.py ================================================ import esphome.codegen as cg from esphome.components import time as time_ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.cpp_generator import get_variable from ..const import CONF_ZIGBEE_ID DEPENDENCIES = ["zigbee"] zigbee_ns = cg.esphome_ns.namespace("zigbee") ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock) ZigBeeComponent = zigbee_ns.class_("ZigBeeComponent", cg.Component) CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ZigbeeTime), cv.GenerateID(CONF_ZIGBEE_ID): cv.use_id(ZigBeeComponent), } ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): cg.add_define("USE_ZIGBEE_TIME") zb = await get_variable(config[CONF_ZIGBEE_ID]) var = cg.new_Pvariable(config[CONF_ID], zb) await cg.register_component(var, config) await time_.register_time(var, config) ================================================ FILE: components/zigbee/time/zigbee_time.cpp ================================================ #include "zigbee_time.h" #include "esphome/core/log.h" namespace esphome { namespace zigbee { void ZigbeeTime::send_timesync_request() { ESP_LOGD(TAG, "Requesting time from coordinator..."); uint16_t attributes[] = {ESP_ZB_ZCL_ATTR_TIME_TIME_ID, ESP_ZB_ZCL_ATTR_TIME_TIME_STATUS_ID}; esp_zb_zcl_read_attr_cmd_t read_req; read_req.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; read_req.attr_field = attributes; read_req.attr_number = sizeof(attributes) / sizeof(uint16_t); read_req.clusterID = ESP_ZB_ZCL_CLUSTER_ID_TIME; read_req.zcl_basic_cmd.dst_endpoint = 1; read_req.zcl_basic_cmd.src_endpoint = 1; read_req.zcl_basic_cmd.dst_addr_u.addr_short = 0x0000; // coordinator if (esp_zb_lock_acquire(30 / portTICK_PERIOD_MS)) { esp_zb_zcl_read_attr_cmd_req(&read_req); esp_zb_lock_release(); this->requested_ = true; ESP_LOGD(TAG, "Sent request"); } } void ZigbeeTime::recieve_timesync_response(esp_zb_zcl_read_attr_resp_variable_t *variable) { uint32_t utc = 0; uint8_t sync_status = 0; while (variable != nullptr) { ESP_LOGD(TAG, "Read attribute response: status(%d), attribute(0x%x), type(0x%x), value(%d)", variable->status, variable->attribute.id, variable->attribute.data.type, variable->attribute.data.value ? *(uint32_t *) variable->attribute.data.value : 0); switch (variable->attribute.id) { case ESP_ZB_ZCL_ATTR_TIME_TIME_ID: utc = *(uint32_t *) variable->attribute.data.value; utc = utc + zigbee_time_offset; ESP_LOGD(TAG, "Recieved UTC time: %d", utc); break; case ESP_ZB_ZCL_ATTR_TIME_TIME_STATUS_ID: sync_status = *(uint8_t *) variable->attribute.data.value; ESP_LOGD(TAG, "Recieved sync status time: 0x%x", sync_status); break; default: ESP_LOGD(TAG, "Recieved other time property: not yet handled"); break; } variable = variable->next; } if ((utc != 0) && (sync_status & 0x3 != 0)) { /* 0x3 = either Master or Syncronized bits set */ this->set_utc_time(utc); } else { ESP_LOGD(TAG, "Did not recieve both time and status; clock NOT updated"); } } void ZigbeeTime::setup() { ESP_LOGD(TAG, "Using Zigbee network as time source"); this->synced_ = false; this->requested_ = false; this->zc_->zt_ = this; } void ZigbeeTime::update() { ESP_LOGD(TAG, "Updating time sync from Zigbee network..."); this->synced_ = false; } void ZigbeeTime::loop() { if (this->synced_) return; if (this->requested_) return; if (zc_->is_connected()) { this->send_timesync_request(); } } void ZigbeeTime::set_utc_time(uint32_t utc) { this->synchronize_epoch_(utc); this->synced_ = true; this->requested_ = false; } } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/time/zigbee_time.h ================================================ #pragma once #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" #include "../zigbee.h" namespace esphome { namespace zigbee { static const uint32_t zigbee_time_offset = 946684800; /* Zigbee time is based on counting seconds from 1 Jan 2000 (=946684800) */ class ZigBeeComponent; class ZigbeeTime : public time::RealTimeClock { public: ZigbeeTime(ZigBeeComponent *zc) : zc_(zc) {} void setup() override; void loop() override; void update() override; void set_utc_time(uint32_t utc); void send_timesync_request(); void recieve_timesync_response(esp_zb_zcl_read_attr_resp_variable_t *variable); protected: ZigBeeComponent *zc_; bool synced_; bool requested_; }; } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/types.py ================================================ from esphome import automation import esphome.codegen as cg zigbee_ns = cg.esphome_ns.namespace("zigbee") ZigBeeComponent = zigbee_ns.class_("ZigBeeComponent", cg.Component) ZigBeeAttribute = zigbee_ns.class_("ZigBeeAttribute", cg.Component) ZigBeeOnValueTrigger = zigbee_ns.class_( "ZigBeeOnValueTrigger", automation.Trigger.template(int), cg.Component ) ZigBeeOnReportTrigger = zigbee_ns.class_( "ZigBeeOnReportTrigger", automation.Trigger.template(int), cg.Component ) ResetZigbeeAction = zigbee_ns.class_( "ResetZigbeeAction", automation.Action, cg.Parented.template(ZigBeeComponent) ) SetAttrAction = zigbee_ns.class_("SetAttrAction", automation.Action) ReportAttrAction = zigbee_ns.class_( "ReportAttrAction", automation.Action, cg.Parented.template(ZigBeeAttribute) ) ReportAction = zigbee_ns.class_( "ReportAction", automation.Action, cg.Parented.template(ZigBeeComponent) ) ================================================ FILE: components/zigbee/zigbee.cpp ================================================ #include "zigbee.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_check.h" #include "nvs_flash.h" #include "zigbee_attribute.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "zigbee_helpers.h" #ifdef CONFIG_WIFI_COEX #include "esp_coexist.h" #endif #if !(defined ZB_ED_ROLE || defined ZB_ROUTER_ROLE) #error Define ZB_ED_ROLE or ZB_ROUTER_ROLE in idf.py menuconfig. #endif namespace esphome { namespace zigbee { ZigBeeComponent *global_zigbee; device_params_t coord; /** * Creates a ZCL string from the given input string. * * @param str Pointer to the input null-terminated C-style string. * @param max_size Maximum allowable size for the resulting ZCL string. Maximum value: 254. * @param use_max_size Optional. If true, the `max_size` is used directly, * overriding the actual size of the input string. * @return Pointer to a dynamically allocated ZCL string. * NOTE: Caller is responsible for freeing the allocated memory with `delete[]`. * */ uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size) { uint8_t str_len = static_cast(strlen(str)); uint8_t zcl_str_size = use_max_size ? max_size : std::min(max_size, str_len); uint8_t *zcl_str = new uint8_t[zcl_str_size + 1]; // string + length octet zcl_str[0] = zcl_str_size; memcpy(zcl_str + 1, str, str_len); return zcl_str; } static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) { if (esp_zb_bdb_start_top_level_commissioning(mode_mask) != ESP_OK) { ESP_LOGE(TAG, "Start network steering failed!"); } } void ZigBeeComponent::report() { for (const auto &[_, attribute] : this->attributes_) { attribute->report(); } } void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) { static uint8_t steering_retry_count = 0; uint32_t *p_sg_p = signal_struct->p_app_signal; esp_err_t err_status = signal_struct->esp_err_status; esp_zb_app_signal_type_t sig_type = (esp_zb_app_signal_type_t) *p_sg_p; esp_zb_zdo_signal_leave_params_t *leave_params = NULL; switch (sig_type) { case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: // Notifies the application that ZBOSS framework (scheduler, buffer pool, etc.) has started, but no // join/rejoin/formation/BDB initialization has been done yet. ESP_LOGD(TAG, "Zigbee stack initialized"); esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); break; case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: // Device started for the first time after the NVRAM erase case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: // Device started using the NVRAM contents. if (err_status == ESP_OK) { ESP_LOGD(TAG, "Device started up in %sfactory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non "); global_zigbee->started_ = true; if (esp_zb_bdb_is_factory_new()) { ESP_LOGD(TAG, "Start network steering"); esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); } else { ESP_LOGD(TAG, "Device rebooted"); global_zigbee->searchBindings(); } } else { ESP_LOGE(TAG, "FIRST_START. Device started up in %sfactory-reset mode with an error %d (%s)", esp_zb_bdb_is_factory_new() ? "" : "non ", err_status, esp_err_to_name(err_status)); ESP_LOGW(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status)); esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_INITIALIZATION, 1000); } break; case ESP_ZB_BDB_SIGNAL_STEERING: // BDB network steering completed (Network steering only) if (err_status == ESP_OK) { steering_retry_count = 0; esp_zb_ieee_addr_t extended_pan_id; esp_zb_get_extended_pan_id(extended_pan_id); ESP_LOGI(TAG, "Joined network successfully (Extended PAN ID: %02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x, PAN ID: " "0x%04hx, Channel:%d)", extended_pan_id[7], extended_pan_id[6], extended_pan_id[5], extended_pan_id[4], extended_pan_id[3], extended_pan_id[2], extended_pan_id[1], extended_pan_id[0], esp_zb_get_pan_id(), esp_zb_get_current_channel()); global_zigbee->joined_ = true; global_zigbee->enable_loop_soon_any_context(); } else { ESP_LOGI(TAG, "Network steering was not successful (status: %s)", esp_err_to_name(err_status)); if (steering_retry_count < 10) { steering_retry_count++; esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_NETWORK_STEERING, 1000); } else { esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_NETWORK_STEERING, 600 * 1000); } } break; case ESP_ZB_ZDO_SIGNAL_LEAVE: leave_params = (esp_zb_zdo_signal_leave_params_t *) esp_zb_app_signal_get_params(p_sg_p); if (leave_params->leave_type == ESP_ZB_NWK_LEAVE_TYPE_RESET) { ESP_LOGD(TAG, "Reset device"); esp_zb_factory_reset(); } else { ESP_LOGD(TAG, "Leave_type: %u", leave_params->leave_type); } break; default: ESP_LOGD(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, esp_err_to_name(err_status)); break; } } // Recall bounded devices from the binding table after reboot void ZigBeeComponent::bindingTableCb(const esp_zb_zdo_binding_table_info_t *table_info, void *user_ctx) { bool done = true; esp_zb_zdo_mgmt_bind_param_t *req = (esp_zb_zdo_mgmt_bind_param_t *) user_ctx; esp_zb_zdp_status_t zdo_status = (esp_zb_zdp_status_t) table_info->status; ESP_LOGD(TAG, "Binding table callback for address 0x%04x with status %d", req->dst_addr, zdo_status); if (zdo_status == ESP_ZB_ZDP_STATUS_SUCCESS) { // Print binding table log simple ESP_LOGD(TAG, "Binding table info: total %d, index %d, count %d", table_info->total, table_info->index, table_info->count); if (table_info->total == 0) { ESP_LOGD(TAG, "No binding table entries found"); free(req); global_zigbee->connected_ = true; return; } esp_zb_zdo_binding_table_record_t *record = table_info->record; for (int i = 0; i < table_info->count; i++) { ESP_LOGD(TAG, "Binding table record: src_endp %d, dst_endp %d, cluster_id 0x%04x, dst_addr_mode %d", record->src_endp, record->dst_endp, record->cluster_id, record->dst_addr_mode); zb_device_params_t *device = (zb_device_params_t *) calloc(1, sizeof(zb_device_params_t)); device->endpoint = record->dst_endp; if (record->dst_addr_mode == ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT || record->dst_addr_mode == ESP_ZB_APS_ADDR_MODE_16_GROUP_ENDP_NOT_PRESENT) { device->short_addr = record->dst_address.addr_short; } else { // ESP_ZB_APS_ADDR_MODE_64_ENDP_PRESENT memcpy(device->ieee_addr, record->dst_address.addr_long, sizeof(esp_zb_ieee_addr_t)); } ESP_LOGD(TAG, "Device bound to EP %d -> device endpoint: %d, short addr: 0x%04x, ieee addr: " "%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X", record->src_endp, device->endpoint, device->short_addr, device->ieee_addr[7], device->ieee_addr[6], device->ieee_addr[5], device->ieee_addr[4], device->ieee_addr[3], device->ieee_addr[2], device->ieee_addr[1], device->ieee_addr[0]); record = record->next; } // Continue reading the binding table if (table_info->index + table_info->count < table_info->total) { /* There are unreported binding table entries, request for them. */ req->start_index = table_info->index + table_info->count; esp_zb_zdo_binding_table_req(req, bindingTableCb, req); done = false; } } if (done) { // Print bound devices ESP_LOGD(TAG, "Filling bounded devices finished"); free(req); global_zigbee->connected_ = true; } } void ZigBeeComponent::searchBindings() { esp_zb_zdo_mgmt_bind_param_t *mb_req = (esp_zb_zdo_mgmt_bind_param_t *) malloc(sizeof(esp_zb_zdo_mgmt_bind_param_t)); mb_req->dst_addr = esp_zb_get_short_address(); mb_req->start_index = 0; ESP_LOGD(TAG, "Requesting binding table for address 0x%04x", mb_req->dst_addr); esp_zb_zdo_binding_table_req(mb_req, bindingTableCb, (void *) mb_req); } void load_zb_event(ZBEvent *event, esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level) { event->load_set_attr_value_event(info, attribute, current_level); } void load_zb_event(ZBEvent *event, const esp_zb_zcl_report_attr_message_t *message) { event->load_report_attr_event(message); } void load_zb_event(ZBEvent *event, esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables) { event->load_read_attr_resp_event(info, variables); } template void enqueue_zb_event(Args... args) { // Allocate an event from the pool ZBEvent *event = global_zigbee->zb_event_pool_.allocate(); if (event == nullptr) { // No events available - queue is full or we're out of memory global_zigbee->zb_events_.increment_dropped_count(); return; } // Load new event data (replaces previous event) load_zb_event(event, args...); // Push the event to the queue global_zigbee->zb_events_.push(event); // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size global_zigbee->enable_loop_soon_any_context(); App.wake_loop_threadsafe(); } // Explicit template instantiations for the friend function template void enqueue_zb_event(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level); template void enqueue_zb_event(const esp_zb_zcl_report_attr_message_t *message); template void enqueue_zb_event(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables); static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message) { ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)", message->info.status); ESP_LOGD(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)", message->info.dst_endpoint, message->info.cluster, message->attribute.id, message->attribute.data.size); // if the attribute is On/Off and it is set to Off, restore the previous level esp_zb_zcl_attr_t *current_level = nullptr; if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF) { if (message->attribute.id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL && !*(bool *) message->attribute.data.value) { ESP_LOGD(TAG, "turned off"); if (esp_zb_zcl_get_cluster(message->info.dst_endpoint, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE) != nullptr) { current_level = esp_zb_zcl_get_attribute(message->info.dst_endpoint, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_LEVEL_CONTROL_CURRENT_LEVEL_ID); if (current_level) { ESP_LOGD(TAG, "got level"); esp_zb_zcl_set_attribute_val(message->info.dst_endpoint, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_LEVEL_CONTROL_CURRENT_LEVEL_ID, current_level->data_p, false); } } } } if (current_level != nullptr) { enqueue_zb_event(message->info, message->attribute, (uint8_t *) current_level->data_p); } else { enqueue_zb_event(message->info, message->attribute, nullptr); } return ESP_OK; } static esp_err_t zb_cmd_attribute_handler(const esp_zb_zcl_cmd_read_attr_resp_message_t *message) { ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)", message->info.status); enqueue_zb_event(message->info, message->variables); return ESP_OK; } static esp_err_t zb_report_attribute_handler(const esp_zb_zcl_report_attr_message_t *message) { ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); ESP_RETURN_ON_FALSE(message->status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)", message->status); enqueue_zb_event(message); return ESP_OK; } static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) { esp_err_t ret = ESP_OK; switch (callback_id) { case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID: ret = zb_attribute_handler((esp_zb_zcl_set_attr_value_message_t *) message); break; case ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID: ret = zb_cmd_attribute_handler((esp_zb_zcl_cmd_read_attr_resp_message_t *) message); break; case ESP_ZB_CORE_REPORT_ATTR_CB_ID: ret = zb_report_attribute_handler((esp_zb_zcl_report_attr_message_t *) message); break; case ESP_ZB_CORE_CMD_DEFAULT_RESP_CB_ID: ESP_LOGD(TAG, "Receive Zigbee default response callback"); break; default: ESP_LOGW(TAG, "Receive Zigbee action(0x%x) callback", callback_id); break; } return ret; } void ZigBeeComponent::handle_attribute(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level) { if (this->attributes_.find({info.dst_endpoint, info.cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, attribute.id}) != this->attributes_.end()) { this->attributes_[{info.dst_endpoint, info.cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, attribute.id}]->on_value( attribute); // if the attribute is On/Off and it is set to Off, restore the previous level if (info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF && current_level != nullptr) { if (attribute.id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID && attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL && !*(bool *) attribute.data.value) { ESP_LOGD(TAG, "turned off"); if (this->attributes_.find({info.dst_endpoint, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_LEVEL_CONTROL_CURRENT_LEVEL_ID}) != this->attributes_.end()) { ESP_LOGD(TAG, "found level"); esp_zb_zcl_attribute_t lvl_attr = { .id = ESP_ZB_ZCL_ATTR_LEVEL_CONTROL_CURRENT_LEVEL_ID, .data = { .type = ESP_ZB_ZCL_ATTR_TYPE_U8, .size = sizeof(uint8_t), .value = current_level, }, }; this->attributes_[{info.dst_endpoint, ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_LEVEL_CONTROL_CURRENT_LEVEL_ID}] ->on_value(lvl_attr); ESP_LOGD(TAG, "Light set to restore-level: %d", *current_level); } } } } } void ZigBeeComponent::handle_report_attribute(uint8_t dst_endpoint, uint16_t cluster, esp_zb_zcl_attribute_t attribute, esp_zb_zcl_addr_t src_address, uint8_t src_endpoint) { auto attr = this->attributes_.find({dst_endpoint, cluster, ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE, attribute.id}); if (attr == this->attributes_.end()) { ESP_LOGD(TAG, "No attributes configured for report (endpoint %d; cluster 0x%04x; attribute id 0x%04x)", dst_endpoint, cluster, attribute.id); return; } attr->second->on_report(attribute, src_address, src_endpoint); } void ZigBeeComponent::handle_read_attribute_response(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables) { switch (info.cluster) { case ESP_ZB_ZCL_CLUSTER_ID_TIME: ESP_LOGD(TAG, "Recieved time information"); #ifdef USE_ZIGBEE_TIME if (this->zt_ == nullptr) { ESP_LOGD(TAG, "No time component linked to update time!"); } else { this->zt_->recieve_timesync_response(variables); } #else ESP_LOGD(TAG, "No zigbee time component included at build time!"); #endif break; default: ESP_LOGD(TAG, "Attribute data recieved (but not yet handled):"); while (variables) { ESP_LOGD(TAG, "Read attribute response: status(%d), cluster(0x%x), attribute(0x%x), type(0x%x), value(%d)", variables->status, info.cluster, variables->attribute.id, variables->attribute.data.type, variables->attribute.data.value ? *(uint8_t *) variables->attribute.data.value : 0); variables = variables->next; } } } void ZigBeeComponent::create_default_cluster(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id) { esp_zb_cluster_list_t *cluster_list = esphome_zb_default_clusters_create(device_id); this->endpoint_list_[endpoint_id] = std::tuple(device_id, cluster_list); // Add basic cluster this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); // Add identify cluster if not already present if (esp_zb_cluster_list_get_cluster(cluster_list, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE) == nullptr) { this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); } } void ZigBeeComponent::add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role) { esp_zb_attribute_list_t *attr_list; switch (cluster_id) { case 0: attr_list = create_basic_cluster_(); break; default: attr_list = esphome_zb_default_attr_list_create(cluster_id); } this->attribute_list_[{endpoint_id, cluster_id, role}] = attr_list; } void ZigBeeComponent::set_basic_cluster(std::string model, std::string manufacturer, std::string date, uint8_t power, uint8_t app_version, uint8_t stack_version, uint8_t hw_version, std::string area, uint8_t physical_env) { this->basic_cluster_data_ = { .model = model, .manufacturer = manufacturer, .date = date, .power = power, .app_version = app_version, .stack_version = stack_version, .hw_version = hw_version, .area = area, .physical_env = physical_env, }; } esp_zb_attribute_list_t *ZigBeeComponent::create_basic_cluster_() { // ------------------------------ Cluster BASIC ------------------------------ esp_zb_basic_cluster_cfg_t basic_cluster_cfg = { .zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE, .power_source = this->basic_cluster_data_.power, }; ESP_LOGD(TAG, "Model: %s", this->basic_cluster_data_.model.c_str()); ESP_LOGD(TAG, "Manufacturer: %s", this->basic_cluster_data_.manufacturer.c_str()); ESP_LOGD(TAG, "Date: %s", this->basic_cluster_data_.date.c_str()); ESP_LOGD(TAG, "Area: %s", this->basic_cluster_data_.area.c_str()); uint8_t *ManufacturerName = get_zcl_string(this->basic_cluster_data_.manufacturer.c_str(), 32); // warning: this is in format {length, 'string'} : uint8_t *ModelIdentifier = get_zcl_string(this->basic_cluster_data_.model.c_str(), 32); uint8_t *DateCode = get_zcl_string(this->basic_cluster_data_.date.c_str(), 16); uint8_t *Location = get_zcl_string(this->basic_cluster_data_.area.c_str(), 16); esp_zb_attribute_list_t *attr_list = esp_zb_basic_cluster_create(&basic_cluster_cfg); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_APPLICATION_VERSION_ID, &(this->basic_cluster_data_.app_version)); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_STACK_VERSION_ID, &(this->basic_cluster_data_.stack_version)); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_HW_VERSION_ID, &(this->basic_cluster_data_.hw_version)); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, ManufacturerName); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, ModelIdentifier); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_DATE_CODE_ID, DateCode); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_LOCATION_DESCRIPTION_ID, Location); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_PHYSICAL_ENVIRONMENT_ID, &(this->basic_cluster_data_.physical_env)); delete[] ManufacturerName; delete[] ModelIdentifier; delete[] DateCode; delete[] Location; return attr_list; } esp_err_t ZigBeeComponent::create_endpoint(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id, esp_zb_cluster_list_t *esp_zb_cluster_list) { // ------------------------------ Create endpoint list ------------------------------ esp_zb_endpoint_config_t endpoint_config = {.endpoint = endpoint_id, .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, .app_device_id = device_id, .app_device_version = this->device_version_}; return esp_zb_ep_list_add_ep(this->esp_zb_ep_list_, esp_zb_cluster_list, endpoint_config); } static void esp_zb_task_(void *pvParameters) { if (esp_zb_start(false) != ESP_OK) { ESP_LOGE(TAG, "Could not setup Zigbee"); // this->mark_failed(); vTaskDelete(NULL); } if ((global_zigbee->device_role_ == ESP_ZB_DEVICE_TYPE_ED) && (global_zigbee->basic_cluster_data_.power == 0x03)) { ESP_LOGD(TAG, "Battery powered!"); // zb_set_ed_node_descriptor(0, 1, 1); // workaround, rx_on_when_idle should be 0 for battery powered devices. esp_zb_set_node_descriptor_power_source(0); } else { esp_zb_set_node_descriptor_power_source(1); } esp_zb_stack_main_loop(); } void ZigBeeComponent::setup() { global_zigbee = this; esp_zb_platform_config_t config = { .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), }; #ifdef CONFIG_WIFI_COEX if (esp_coex_wifi_i154_enable() != ESP_OK) { this->mark_failed(); return; } #endif // ESP_ERROR_CHECK(nvs_flash_init()); not needed, called by esp32 component if (esp_zb_platform_config(&config) != ESP_OK) { this->mark_failed(); return; } /* initialize Zigbee stack */ esp_zb_zed_cfg_t zb_zed_cfg = { .ed_timeout = ED_AGING_TIMEOUT, .keep_alive = ED_KEEP_ALIVE, }; esp_zb_zczr_cfg_t zb_zczr_cfg = { .max_children = MAX_CHILDREN, }; esp_zb_cfg_t zb_nwk_cfg = { .esp_zb_role = this->device_role_, .install_code_policy = INSTALLCODE_POLICY_ENABLE, }; #ifdef ZB_ROUTER_ROLE zb_nwk_cfg.nwk_cfg.zczr_cfg = zb_zczr_cfg; #else zb_nwk_cfg.nwk_cfg.zed_cfg = zb_zed_cfg; #endif esp_zb_init(&zb_nwk_cfg); if (this->custom_trust_center_key_) { esp_zb_enable_joining_to_distributed(true); esp_zb_secur_TC_standard_distributed_key_set(this->trustkey_); } esp_err_t ret; // clusters for (auto const &[key, val] : this->attribute_list_) { esp_zb_cluster_list_t *esp_zb_cluster_list = std::get<1>(this->endpoint_list_[std::get<0>(key)]); ret = esphome_zb_cluster_list_add_or_update_cluster(std::get<1>(key), esp_zb_cluster_list, val, std::get<2>(key)); if (ret != ESP_OK) { ESP_LOGE(TAG, "Could not create cluster 0x%04X with role %u: %s", std::get<1>(key), std::get<2>(key), esp_err_to_name(ret)); } } this->attribute_list_.clear(); // endpoints for (auto const &[ep_id, dev] : this->endpoint_list_) { // create_default_cluster(key, val); if (create_endpoint(ep_id, std::get<0>(dev), std::get<1>(dev)) != ESP_OK) { ESP_LOGE(TAG, "Could not create endpoint %u", ep_id); } } // ------------------------------ Register Device ------------------------------ if (esp_zb_device_register(this->esp_zb_ep_list_) != ESP_OK) { ESP_LOGE(TAG, "Could not register the endpoint list"); this->mark_failed(); return; } esp_zb_core_action_handler_register(zb_action_handler); if (esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK) != ESP_OK) { ESP_LOGE(TAG, "Could not setup Zigbee"); this->mark_failed(); return; } // reporting for (auto &[_, attribute] : this->attributes_) { if (attribute->report_enabled) { esp_zb_zcl_reporting_info_t reporting_info = attribute->get_reporting_info(); ESP_LOGD(TAG, "set reporting for cluster: %u", reporting_info.cluster_id); if (esp_zb_zcl_update_reporting_info(&reporting_info) != ESP_OK) { ESP_LOGE(TAG, "Could not configure reporting for attribute 0x%04X in cluster 0x%04X in endpoint %u", reporting_info.attr_id, reporting_info.cluster_id, reporting_info.ep); } } } xTaskCreate(esp_zb_task_, "Zigbee_main", 4096, NULL, 24, NULL); this->disable_loop(); // loop is only needed for processing events, so disable until we join a network } void ZigBeeComponent::loop() { // Process all pending events ZBEvent *event = this->zb_events_.pop(); while (event != nullptr) { // Handle the event switch (event->callback_id_) { case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID: this->handle_attribute( event->event_.set_attr.info, event->event_.set_attr.attribute, event->event_.set_attr.has_current_level ? &event->event_.set_attr.current_level : nullptr); break; case ESP_ZB_CORE_CMD_READ_ATTR_RESP_CB_ID: this->handle_read_attribute_response(event->event_.read_attr_resp.info, &(event->event_.read_attr_resp.variables)); break; case ESP_ZB_CORE_REPORT_ATTR_CB_ID: this->handle_report_attribute(event->event_.report_attr.dst_endpoint, event->event_.report_attr.cluster, event->event_.report_attr.attribute, event->event_.report_attr.src_address, event->event_.report_attr.src_endpoint); break; } // Free the event back to the pool this->zb_event_pool_.release(event); // Get the next event event = this->zb_events_.pop(); } // Log dropped events periodically uint16_t dropped = this->zb_events_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u Zigbee events due to buffer overflow", dropped); } if (this->joined_) { this->on_join_callback_.call(); this->joined_ = false; // only call once this->connected_ = true; } else if (this->connected_) { this->disable_loop(); // only disable once connected } } void ZigBeeComponent::dump_config() { char trustkey_hex[format_hex_pretty_size(sizeof(this->trustkey_))]; ESP_LOGCONFIG(TAG, "ZigBee:"); ESP_LOGCONFIG(TAG, " Device Version: %u", this->device_version_); if (this->custom_trust_center_key_) { ESP_LOGCONFIG(TAG, " Custom Trust Center Key: %s", format_hex_pretty_to(trustkey_hex, this->trustkey_, sizeof(this->trustkey_), '.')); } for (auto const &[key, val] : this->endpoint_list_) { ESP_LOGCONFIG(TAG, " Endpoint: %u, %d", key, std::get<0>(val)); } } void ZigBeeComponent::set_trust_center_key(const char *trust_center_key) { parse_hex(trust_center_key, this->trustkey_, sizeof(this->trustkey_)); this->custom_trust_center_key_ = true; } } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/zigbee.h ================================================ #pragma once #include #include #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/core/lock_free_queue.h" #include "esphome/core/event_pool.h" #include "esp_zb_event.h" #include "esp_zigbee_core.h" #include "zboss_api.h" #include "ha/esp_zigbee_ha_standard.h" #include "zigbee_helpers.h" #ifdef USE_ZIGBEE_TIME #include "time/zigbee_time.h" #endif namespace esphome { namespace zigbee { static const char *const TAG = "zigbee"; static constexpr uint8_t MAX_ZB_QUEUE_SIZE = 32; using device_params_t = struct DeviceParamsS { esp_zb_ieee_addr_t ieee_addr; uint8_t endpoint; uint16_t short_addr; }; using zdo_info_user_ctx_t = struct ZdoInfoCtxS { uint8_t endpoint; uint16_t short_addr; }; using zb_device_params_t = struct zb_device_params_s { esp_zb_ieee_addr_t ieee_addr; uint8_t endpoint; uint16_t short_addr; }; /* Zigbee configuration */ #define INSTALLCODE_POLICY_ENABLE false /* enable the install code policy for security */ #define ED_AGING_TIMEOUT ESP_ZB_ED_AGING_TIMEOUT_64MIN #define ED_KEEP_ALIVE 3000 /* 3000 millisecond */ #define MAX_CHILDREN 10 #define ESP_ZB_PRIMARY_CHANNEL_MASK \ ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK /* Zigbee primary channel mask use in the example */ #define ESP_ZB_DEFAULT_RADIO_CONFIG() \ { .radio_mode = ZB_RADIO_MODE_NATIVE, } #define ESP_ZB_DEFAULT_HOST_CONFIG() \ { .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, } template T get_value_by_type(uint8_t attr_type, void *data); uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size = false); class ZigBeeAttribute; class ZigbeeTime; class ZigBeeComponent : public Component { public: void setup() override; void loop() override; void dump_config() override; esp_err_t create_endpoint(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id, esp_zb_cluster_list_t *esp_zb_cluster_list); void set_basic_cluster(std::string model, std::string manufacturer, std::string date, uint8_t power, uint8_t app_version, uint8_t stack_version, uint8_t hw_version, std::string area, uint8_t physical_env); void set_trust_center_key(const char *trust_center_key); void set_device_version(uint8_t version) { this->device_version_ = version; } void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role); void create_default_cluster(uint8_t endpoint_id, esp_zb_ha_standard_devices_t device_id); template void add_attr(ZigBeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, uint8_t max_size, T value); template void add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, uint8_t max_size, T value); void handle_attribute(esp_zb_device_cb_common_info_t info, esp_zb_zcl_attribute_t attribute, uint8_t *current_level); void handle_report_attribute(uint8_t dst_endpoint, uint16_t cluster, esp_zb_zcl_attribute_t attribute, esp_zb_zcl_addr_t src_address, uint8_t src_endpoint); void handle_read_attribute_response(esp_zb_zcl_cmd_info_t info, esp_zb_zcl_read_attr_resp_variable_t *variables); void searchBindings(); static void bindingTableCb(const esp_zb_zdo_binding_table_info_t *table_info, void *user_ctx); void reset() { esp_zb_lock_acquire(portMAX_DELAY); esp_zb_factory_reset(); esp_zb_lock_release(); } void report(); #ifdef USE_ZIGBEE_TIME ZigbeeTime *zt_{nullptr}; #endif template void add_on_join_callback(F &&callback) { this->on_join_callback_.add(std::forward(callback)); } bool is_started() { return this->started_; } bool is_connected() { return this->connected_; } bool connected_ = false; bool started_ = false; bool joined_ = false; CallbackManager on_join_callback_{}; struct { std::string model; std::string manufacturer; std::string date; uint8_t power; uint8_t app_version; uint8_t stack_version; uint8_t hw_version; std::string area; uint8_t physical_env; } basic_cluster_data_; #ifdef ZB_ED_ROLE esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ED; #else esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ROUTER; #endif protected: template friend void enqueue_zb_event(Args... args); esphome::LockFreeQueue zb_events_; esphome::EventPool zb_event_pool_; esp_zb_attribute_list_t *create_basic_cluster_(); template void add_attr_(ZigBeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, T *value_p); std::map> endpoint_list_; std::map, esp_zb_attribute_list_t *> attribute_list_; std::map, ZigBeeAttribute *> attributes_; esp_zb_ep_list_t *esp_zb_ep_list_ = esp_zb_ep_list_create(); uint8_t ident_time_; bool custom_trust_center_key_ = false; uint8_t trustkey_[16] = {0}; uint8_t device_version_ = 0; }; extern "C" void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct); extern "C" void zb_set_ed_node_descriptor(bool power_src, bool rx_on_when_idle, bool alloc_addr); template void ZigBeeComponent::add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, uint8_t max_size, T value) { this->add_attr(nullptr, endpoint_id, cluster_id, role, attr_id, attr_type, attr_access, max_size, value); } template void ZigBeeComponent::add_attr(ZigBeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, uint8_t max_size, T value) { // The size byte of the zcl_str must be set to the maximum value, // even though the initial string may be shorter. if constexpr (std::is_same::value) { auto zcl_str = get_zcl_string(value.c_str(), max_size, true); add_attr_(attr, endpoint_id, cluster_id, role, attr_id, attr_type, attr_access, zcl_str); delete[] zcl_str; } else if constexpr (std::is_convertible::value) { auto zcl_str = get_zcl_string(value, max_size, true); add_attr_(attr, endpoint_id, cluster_id, role, attr_id, attr_type, attr_access, zcl_str); delete[] zcl_str; } else { add_attr_(attr, endpoint_id, cluster_id, role, attr_id, attr_type, attr_access, &value); } } template void ZigBeeComponent::add_attr_(ZigBeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, T *value_p) { esp_zb_attribute_list_t *attr_list = this->attribute_list_[{endpoint_id, cluster_id, role}]; esp_err_t ret = esphome_zb_cluster_add_or_update_attr(cluster_id, attr_list, attr_id, attr_type, attr_access, value_p); if (ret != ESP_OK) { ESP_LOGE(TAG, "Could not add attribute 0x%04X to cluster 0x%04X in endpoint %u: %s", attr_id, cluster_id, endpoint_id, esp_err_to_name(ret)); } if (attr != nullptr) { this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr; } } } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/zigbee_attribute.cpp ================================================ #include "zigbee_attribute.h" namespace esphome { namespace zigbee { void ZigBeeAttribute::set_attr_() { if (!this->zb_->is_connected()) { return; } if (esp_zb_lock_acquire(20 / portTICK_PERIOD_MS)) { esp_zb_zcl_status_t state = esp_zb_zcl_set_attribute_val(this->endpoint_id_, this->cluster_id_, this->role_, this->attr_id_, this->value_p, false); if (this->force_report_) { this->report_(true); } this->set_attr_requested_ = false; // Check for error if (state != ESP_ZB_ZCL_STATUS_SUCCESS) { ESP_LOGE(TAG, "Setting attribute failed: %s", esp_err_to_name(state)); } ESP_LOGD(TAG, "Attribute set!"); esp_zb_lock_release(); } } void ZigBeeAttribute::report_() { this->report_(false); } void ZigBeeAttribute::report_(bool has_lock) { if (!this->zb_->is_connected()) { return; } if (has_lock or esp_zb_lock_acquire(20 / portTICK_PERIOD_MS)) { esp_zb_zcl_report_attr_cmd_t cmd = { .address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT, .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI, }; cmd.zcl_basic_cmd.dst_addr_u.addr_short = 0x0000; cmd.zcl_basic_cmd.dst_endpoint = 1; cmd.zcl_basic_cmd.src_endpoint = this->endpoint_id_; cmd.clusterID = this->cluster_id_; cmd.attributeID = this->attr_id_; // cmd.cluster_role = reporting_info.cluster_role; esp_zb_zcl_report_attr_cmd_req(&cmd); this->report_requested_ = false; if (!has_lock) { esp_zb_lock_release(); } } } esp_zb_zcl_reporting_info_t ZigBeeAttribute::get_reporting_info() { esp_zb_zcl_reporting_info_t reporting_info = { .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV, .ep = this->endpoint_id_, .cluster_id = this->cluster_id_, .cluster_role = this->role_, .attr_id = this->attr_id_, .manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC, }; reporting_info.dst.profile_id = ESP_ZB_AF_HA_PROFILE_ID; reporting_info.u.send_info.min_interval = 10; /*!< Actual minimum reporting interval */ reporting_info.u.send_info.max_interval = 0; /*!< Actual maximum reporting interval */ reporting_info.u.send_info.def_min_interval = 10; /*!< Default minimum reporting interval */ reporting_info.u.send_info.def_max_interval = 0; /*!< Default maximum reporting interval */ reporting_info.u.send_info.delta.s16 = 0; /*!< Actual reportable change */ return reporting_info; } void ZigBeeAttribute::set_report(bool force) { this->report_enabled = true; this->force_report_ = force; } void ZigBeeAttribute::report() { this->report_requested_ = true; this->enable_loop(); } void ZigBeeAttribute::loop() { if (this->set_attr_requested_) { this->set_attr_(); } if (this->report_requested_) { this->report_(); } if (!this->report_requested_ && !this->set_attr_requested_) { this->disable_loop(); } } } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/zigbee_attribute.h ================================================ #pragma once #include #include "zigbee.h" #include "esp_zigbee_core.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif #ifdef USE_SWITCH #include "esphome/components/switch/switch.h" #endif #ifdef USE_LIGHT #include "esphome/components/light/light_state.h" #endif namespace esphome { namespace zigbee { #ifdef USE_LIGHT void set_light_color(uint8_t ep, light::LightCall *call, uint16_t value, bool is_x); #endif class ZigBeeAttribute : public Component { public: ZigBeeAttribute(ZigBeeComponent *parent, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t attr_type, float scale) : zb_(parent), endpoint_id_(endpoint_id), cluster_id_(cluster_id), role_(role), attr_id_(attr_id), attr_type_(attr_type), scale_(scale) {} // void dump_config() override; void loop() override; template void add_attr(uint8_t attr_access, uint8_t max_size, T value); esp_zb_zcl_reporting_info_t get_reporting_info(); void set_report(bool force); void report(); template void set_attr(const T &value); uint8_t attr_type() { return attr_type_; } template void add_on_value_callback(F &&callback) { on_value_callback_.add(std::forward(callback)); } void on_value(esp_zb_zcl_attribute_t attribute) { this->on_value_callback_.call(attribute); } void add_on_report_callback( std::function callback) { on_report_callback_.add(std::move(callback)); } void on_report(esp_zb_zcl_attribute_t attribute, esp_zb_zcl_addr_t src_address, uint8_t src_endpoint) { this->on_report_callback_.call(attribute, src_address, src_endpoint); } bool report_enabled = false; #ifdef USE_SENSOR template void connect(sensor::Sensor *sensor); template void connect(sensor::Sensor *sensor, std::function &&f); #endif #ifdef USE_BINARY_SENSOR template void connect(binary_sensor::BinarySensor *sensor); template void connect(binary_sensor::BinarySensor *sensor, std::function &&f); #endif #ifdef USE_TEXT_SENSOR template void connect(text_sensor::TextSensor *sensor); template void connect(text_sensor::TextSensor *sensor, std::function &&f); #endif #ifdef USE_SWITCH template void connect(switch_::Switch *device); template void connect(switch_::Switch *device, std::function &&f); #endif #ifdef USE_LIGHT template void connect(light::LightState *device); #endif protected: void set_attr_(); void report_(); void report_(bool has_lock); ZigBeeComponent *zb_; uint8_t endpoint_id_; uint16_t cluster_id_; uint8_t role_; uint16_t attr_id_; uint8_t attr_type_; uint8_t max_size_; float scale_; CallbackManager on_value_callback_{}; CallbackManager on_report_callback_{}; void *value_p{nullptr}; bool set_attr_requested_{false}; bool report_requested_{false}; bool force_report_{false}; }; template void ZigBeeAttribute::add_attr(uint8_t attr_access, uint8_t max_size, T value) { this->max_size_ = max_size; this->zb_->add_attr(this, this->endpoint_id_, this->cluster_id_, this->role_, this->attr_id_, this->attr_type_, attr_access, max_size, std::move(value)); } template void ZigBeeAttribute::set_attr(const T &value) { if constexpr (std::is_convertible::value) { auto zcl_str = get_zcl_string(value, this->max_size_); if (this->value_p != nullptr) { delete[](char *) this->value_p; } this->value_p = (void *) zcl_str; } else if constexpr (std::is_same::value) { auto zcl_str = get_zcl_string(value.c_str(), this->max_size_); if (this->value_p != nullptr) { delete[](char *) this->value_p; } this->value_p = (void *) zcl_str; } else { if (this->value_p != nullptr) { delete (T *) this->value_p; } T *value_p = new T; *value_p = value; this->value_p = (void *) value_p; } this->set_attr_requested_ = true; this->enable_loop(); } #ifdef USE_SENSOR template void ZigBeeAttribute::connect(sensor::Sensor *sensor) { sensor->add_on_state_callback([=, this](float value) { this->set_attr((T) (this->scale_ * value)); }); } template void ZigBeeAttribute::connect(sensor::Sensor *sensor, std::function &&f) { sensor->add_on_state_callback([=, this](float value) { this->set_attr(f(value)); }); } #endif #ifdef USE_BINARY_SENSOR template void ZigBeeAttribute::connect(binary_sensor::BinarySensor *sensor) { sensor->add_on_state_callback([=, this](bool value) { this->set_attr((T) (this->scale_ * value)); }); } template void ZigBeeAttribute::connect(binary_sensor::BinarySensor *sensor, std::function &&f) { sensor->add_on_state_callback([=, this](bool value) { this->set_attr(f(value)); }); } #endif #ifdef USE_TEXT_SENSOR template void ZigBeeAttribute::connect(text_sensor::TextSensor *sensor) { sensor->add_on_state_callback([=, this](std::string value) { this->set_attr((T) (value)); }); } template void ZigBeeAttribute::connect(text_sensor::TextSensor *sensor, std::function &&f) { sensor->add_on_state_callback([=, this](std::string value) { this->set_attr(f(value)); }); } #endif #ifdef USE_SWITCH template void ZigBeeAttribute::connect(switch_::Switch *device) { this->add_on_value_callback([=, this](esp_zb_zcl_attribute_t attribute) { if (attribute.data.type == this->attr_type() && attribute.data.value) { if (get_value_by_type(this->attr_type(), attribute.data.value)) { device->turn_on(); } else { device->turn_off(); } } }); device->add_on_state_callback([=, this](bool value) { this->set_attr((T) (this->scale_ * value)); }); } template void ZigBeeAttribute::connect(switch_::Switch *device, std::function &&f) { this->add_on_value_callback([=, this](esp_zb_zcl_attribute_t attribute) { if (attribute.data.type == this->attr_type() && attribute.data.value) { if (f(get_value_by_type(this->attr_type(), attribute.data.value))) { device->turn_on(); } else { device->turn_off(); } } }); } #endif #ifdef USE_LIGHT template void ZigBeeAttribute::connect(light::LightState *device) { this->add_on_value_callback([=, this](esp_zb_zcl_attribute_t attribute) { if (attribute.data.type == this->attr_type() && attribute.data.value) { light::LightCall call = device->make_call(); ESP_LOGD(TAG, "Make light call"); if (std::is_same::value) { call.set_state(get_value_by_type(this->attr_type(), attribute.data.value)); ESP_LOGD(TAG, "Set state"); } else if (this->cluster_id_ == 0x0300 && this->attr_id_ == 0x3) { // set X set_light_color(this->endpoint_id_, &call, get_value_by_type(this->attr_type(), attribute.data.value), true); ESP_LOGD(TAG, "Set X"); } else if (this->cluster_id_ == 0x0300 && this->attr_id_ == 0x4) { // set Y set_light_color(this->endpoint_id_, &call, get_value_by_type(this->attr_type(), attribute.data.value), false); ESP_LOGD(TAG, "Set Y"); } else if (this->cluster_id_ != 0x0300 and std::numeric_limits::is_integer) { call.set_brightness((float) get_value_by_type(this->attr_type(), attribute.data.value) / 255); // integer level between 0 and 255 ESP_LOGD(TAG, "Set level: %f", (float) get_value_by_type(this->attr_type(), attribute.data.value) / 255); //} else if (this->cluster_id_ != 0x0300 and std::is_floating_point) { // call.set_brightness(get_value_by_type(this->attr_type(), attribute.data.value)); //float level between 0 // and 1 } call.perform(); } }); } #endif } // namespace zigbee } // namespace esphome ================================================ FILE: components/zigbee/zigbee_const.py ================================================ import esphome.codegen as cg ha_standard_devices = cg.esphome_ns.enum("esp_zb_ha_standard_devices_t") DEVICE_ID = { "ON_OFF_SWITCH": ha_standard_devices.ESP_ZB_HA_ON_OFF_SWITCH_DEVICE_ID, 0x0000: ha_standard_devices.ESP_ZB_HA_ON_OFF_SWITCH_DEVICE_ID, "LEVEL_CONTROL_SWITCH": ha_standard_devices.ESP_ZB_HA_LEVEL_CONTROL_SWITCH_DEVICE_ID, 0x0001: ha_standard_devices.ESP_ZB_HA_LEVEL_CONTROL_SWITCH_DEVICE_ID, "ON_OFF_OUTPUT": ha_standard_devices.ESP_ZB_HA_ON_OFF_OUTPUT_DEVICE_ID, 0x0002: ha_standard_devices.ESP_ZB_HA_ON_OFF_OUTPUT_DEVICE_ID, "LEVEL_CONTROLLABLE_OUTPUT": ha_standard_devices.ESP_ZB_HA_LEVEL_CONTROLLABLE_OUTPUT_DEVICE_ID, 0x0003: ha_standard_devices.ESP_ZB_HA_LEVEL_CONTROLLABLE_OUTPUT_DEVICE_ID, "SCENE_SELECTOR": ha_standard_devices.ESP_ZB_HA_SCENE_SELECTOR_DEVICE_ID, 0x0004: ha_standard_devices.ESP_ZB_HA_SCENE_SELECTOR_DEVICE_ID, "CONFIGURATION_TOOL": ha_standard_devices.ESP_ZB_HA_CONFIGURATION_TOOL_DEVICE_ID, 0x0005: ha_standard_devices.ESP_ZB_HA_CONFIGURATION_TOOL_DEVICE_ID, "REMOTE_CONTROL": ha_standard_devices.ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID, 0x0006: ha_standard_devices.ESP_ZB_HA_REMOTE_CONTROL_DEVICE_ID, "COMBINED_INTERFACE": ha_standard_devices.ESP_ZB_HA_COMBINED_INTERFACE_DEVICE_ID, 0x0007: ha_standard_devices.ESP_ZB_HA_COMBINED_INTERFACE_DEVICE_ID, "RANGE_EXTENDER": ha_standard_devices.ESP_ZB_HA_RANGE_EXTENDER_DEVICE_ID, 0x0008: ha_standard_devices.ESP_ZB_HA_RANGE_EXTENDER_DEVICE_ID, "MAINS_POWER_OUTLET": ha_standard_devices.ESP_ZB_HA_MAINS_POWER_OUTLET_DEVICE_ID, 0x0009: ha_standard_devices.ESP_ZB_HA_MAINS_POWER_OUTLET_DEVICE_ID, "DOOR_LOCK": ha_standard_devices.ESP_ZB_HA_DOOR_LOCK_DEVICE_ID, 0x000A: ha_standard_devices.ESP_ZB_HA_DOOR_LOCK_DEVICE_ID, "DOOR_LOCK_CONTROLLER": ha_standard_devices.ESP_ZB_HA_DOOR_LOCK_CONTROLLER_DEVICE_ID, 0x000B: ha_standard_devices.ESP_ZB_HA_DOOR_LOCK_CONTROLLER_DEVICE_ID, "SIMPLE_SENSOR": ha_standard_devices.ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID, 0x000C: ha_standard_devices.ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID, "CONSUMPTION_AWARENESS": ha_standard_devices.ESP_ZB_HA_CONSUMPTION_AWARENESS_DEVICE_ID, 0x000D: ha_standard_devices.ESP_ZB_HA_CONSUMPTION_AWARENESS_DEVICE_ID, "HOME_GATEWAY": ha_standard_devices.ESP_ZB_HA_HOME_GATEWAY_DEVICE_ID, 0x0050: ha_standard_devices.ESP_ZB_HA_HOME_GATEWAY_DEVICE_ID, "SMART_PLUG": ha_standard_devices.ESP_ZB_HA_SMART_PLUG_DEVICE_ID, 0x0051: ha_standard_devices.ESP_ZB_HA_SMART_PLUG_DEVICE_ID, "WHITE_GOODS": ha_standard_devices.ESP_ZB_HA_WHITE_GOODS_DEVICE_ID, 0x0052: ha_standard_devices.ESP_ZB_HA_WHITE_GOODS_DEVICE_ID, "METER_INTERFACE": ha_standard_devices.ESP_ZB_HA_METER_INTERFACE_DEVICE_ID, 0x0053: ha_standard_devices.ESP_ZB_HA_METER_INTERFACE_DEVICE_ID, "ON_OFF_LIGHT": ha_standard_devices.ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID, 0x0100: ha_standard_devices.ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID, "DIMMABLE_LIGHT": ha_standard_devices.ESP_ZB_HA_DIMMABLE_LIGHT_DEVICE_ID, 0x0101: ha_standard_devices.ESP_ZB_HA_DIMMABLE_LIGHT_DEVICE_ID, "COLOR_DIMMABLE_LIGHT": ha_standard_devices.ESP_ZB_HA_COLOR_DIMMABLE_LIGHT_DEVICE_ID, 0x0102: ha_standard_devices.ESP_ZB_HA_COLOR_DIMMABLE_LIGHT_DEVICE_ID, "DIMMER_SWITCH": ha_standard_devices.ESP_ZB_HA_DIMMER_SWITCH_DEVICE_ID, 0x0104: ha_standard_devices.ESP_ZB_HA_DIMMER_SWITCH_DEVICE_ID, "COLOR_DIMMER_SWITCH": ha_standard_devices.ESP_ZB_HA_COLOR_DIMMER_SWITCH_DEVICE_ID, 0x0105: ha_standard_devices.ESP_ZB_HA_COLOR_DIMMER_SWITCH_DEVICE_ID, "LIGHT_SENSOR": ha_standard_devices.ESP_ZB_HA_LIGHT_SENSOR_DEVICE_ID, 0x0106: ha_standard_devices.ESP_ZB_HA_LIGHT_SENSOR_DEVICE_ID, "SHADE": ha_standard_devices.ESP_ZB_HA_SHADE_DEVICE_ID, 0x0200: ha_standard_devices.ESP_ZB_HA_SHADE_DEVICE_ID, "SHADE_CONTROLLER": ha_standard_devices.ESP_ZB_HA_SHADE_CONTROLLER_DEVICE_ID, 0x0201: ha_standard_devices.ESP_ZB_HA_SHADE_CONTROLLER_DEVICE_ID, "WINDOW_COVERING": ha_standard_devices.ESP_ZB_HA_WINDOW_COVERING_DEVICE_ID, 0x0202: ha_standard_devices.ESP_ZB_HA_WINDOW_COVERING_DEVICE_ID, "WINDOW_COVERING_CONTROLLER": ha_standard_devices.ESP_ZB_HA_WINDOW_COVERING_CONTROLLER_DEVICE_ID, 0x0203: ha_standard_devices.ESP_ZB_HA_WINDOW_COVERING_CONTROLLER_DEVICE_ID, "HEATING_COOLING_UNIT": ha_standard_devices.ESP_ZB_HA_HEATING_COOLING_UNIT_DEVICE_ID, 0x0300: ha_standard_devices.ESP_ZB_HA_HEATING_COOLING_UNIT_DEVICE_ID, "THERMOSTAT": ha_standard_devices.ESP_ZB_HA_THERMOSTAT_DEVICE_ID, 0x0301: ha_standard_devices.ESP_ZB_HA_THERMOSTAT_DEVICE_ID, "TEMPERATURE_SENSOR": ha_standard_devices.ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID, 0x0302: ha_standard_devices.ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID, "IAS_CONTROL_INDICATING_EQUIPMENT_ID": ha_standard_devices.ESP_ZB_HA_IAS_CONTROL_INDICATING_EQUIPMENT_ID, 0x0400: ha_standard_devices.ESP_ZB_HA_IAS_CONTROL_INDICATING_EQUIPMENT_ID, "IAS_ANCILLARY_CONTROL_EQUIPMENT_ID": ha_standard_devices.ESP_ZB_HA_IAS_ANCILLARY_CONTROL_EQUIPMENT_ID, 0x0401: ha_standard_devices.ESP_ZB_HA_IAS_ANCILLARY_CONTROL_EQUIPMENT_ID, "IAS_ZONE_ID": ha_standard_devices.ESP_ZB_HA_IAS_ZONE_ID, 0x0402: ha_standard_devices.ESP_ZB_HA_IAS_ZONE_ID, "IAS_WARNING": ha_standard_devices.ESP_ZB_HA_IAS_WARNING_DEVICE_ID, 0x0403: ha_standard_devices.ESP_ZB_HA_IAS_WARNING_DEVICE_ID, "TEST": ha_standard_devices.ESP_ZB_HA_TEST_DEVICE_ID, 0xFFF0: ha_standard_devices.ESP_ZB_HA_TEST_DEVICE_ID, "CUSTOM_TUNNEL": ha_standard_devices.ESP_ZB_HA_CUSTOM_TUNNEL_DEVICE_ID, 0xFFF1: ha_standard_devices.ESP_ZB_HA_CUSTOM_TUNNEL_DEVICE_ID, "CUSTOM_ATTR": ha_standard_devices.ESP_ZB_HA_CUSTOM_ATTR_DEVICE_ID, 0xFFF2: ha_standard_devices.ESP_ZB_HA_CUSTOM_ATTR_DEVICE_ID, } cluster_id = cg.esphome_ns.enum("esp_zb_zcl_cluster_id_t") CLUSTER_ID = { "BASIC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC, 0x0000: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC, "POWER_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG, 0x0001: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG, "DEVICE_TEMP_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG, 0x0002: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG, "IDENTIFY": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, 0x0003: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, "GROUPS": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_GROUPS, 0x0004: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_GROUPS, "SCENES": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_SCENES, 0x0005: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_SCENES, "ON_OFF": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ON_OFF, 0x0006: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ON_OFF, "ON_OFF_SWITCH_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG, 0x0007: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG, "LEVEL_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, 0x0008: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL, "ALARMS": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ALARMS, 0x0009: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ALARMS, "TIME": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_TIME, 0x000A: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_TIME, "RSSI_LOCATION": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_RSSI_LOCATION, 0x000B: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_RSSI_LOCATION, "ANALOG_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, 0x000C: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, "ANALOG_OUTPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, 0x000D: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, "ANALOG_VALUE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE, 0x000E: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE, "BINARY_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT, 0x000F: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT, "BINARY_OUTPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, 0x0010: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, "BINARY_VALUE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE, 0x0011: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE, "MULTI_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, 0x0012: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT, "MULTI_OUTPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, 0x0013: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT, "MULTI_VALUE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE, 0x0014: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE, "COMMISSIONING": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING, 0x0015: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING, "OTA_UPGRADE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE, 0x0019: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE, "POLL_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL, 0x0020: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL, "GREEN_POWER": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_GREEN_POWER, 0x0021: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_GREEN_POWER, "KEEP_ALIVE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_KEEP_ALIVE, 0x0025: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_KEEP_ALIVE, "SHADE_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG, 0x0100: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG, "DOOR_LOCK": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK, 0x0101: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK, "WINDOW_COVERING": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING, 0x0102: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING, "PUMP_CONFIG_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PUMP_CONFIG_CONTROL, 0x0200: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PUMP_CONFIG_CONTROL, "THERMOSTAT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT, 0x0201: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT, "FAN_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL, 0x0202: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL, "DEHUMIDIFICATION_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DEHUMIDIFICATION_CONTROL, 0x0203: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DEHUMIDIFICATION_CONTROL, "THERMOSTAT_UI_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG, 0x0204: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG, "COLOR_CONTROL": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL, 0x0300: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL, "BALLAST_CONFIG": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BALLAST_CONFIG, 0x0301: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BALLAST_CONFIG, "ILLUMINANCE_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT, 0x0400: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT, "TEMP_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT, 0x0402: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT, "PRESSURE_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT, 0x0403: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT, "FLOW_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT, 0x0404: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT, "REL_HUMIDITY_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT, 0x0405: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT, "OCCUPANCY_SENSING": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, 0x0406: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING, "PH_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT, 0x0409: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT, "EC_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT, 0x040A: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT, "WIND_SPEED_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT, 0x040B: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT, "CARBON_DIOXIDE_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT, 0x040D: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT, "PM2_5_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT, 0x042A: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT, "IAS_ZONE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE, 0x0500: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE, "IAS_ACE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_ACE, 0x0501: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_ACE, "IAS_WD": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_WD, 0x0502: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_IAS_WD, "PRICE": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PRICE, 0x0700: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_PRICE, "DRLC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DRLC, 0x0701: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DRLC, "METERING": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_METERING, 0x0702: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_METERING, "METER_IDENTIFICATION": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION, 0x0B01: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION, "ELECTRICAL_MEASUREMENT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT, 0x0B04: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT, "DIAGNOSTICS": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS, 0x0B05: cluster_id.ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS, } cluster_role = cg.esphome_ns.enum("esp_zb_zcl_cluster_role_t") CLUSTER_ROLE = { "SERVER": cluster_role.ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, "CLIENT": cluster_role.ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE, } attr_type = cg.esphome_ns.enum("esp_zb_zcl_attr_type_t") ATTR_TYPE = { "NULL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_NULL, "8BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BIT, "16BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_16BIT, "24BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_24BIT, "32BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_32BIT, "40BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_40BIT, "48BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_48BIT, "56BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_56BIT, "64BIT": attr_type.ESP_ZB_ZCL_ATTR_TYPE_64BIT, "BOOL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BOOL, "8BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BITMAP, "16BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_16BITMAP, "24BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_24BITMAP, "32BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_32BITMAP, "40BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_40BITMAP, "48BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_48BITMAP, "56BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_56BITMAP, "64BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_64BITMAP, "U8": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U8, "U16": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U16, "U24": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U24, "U32": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U32, "U40": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U40, "U48": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U48, "U56": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U56, "U64": attr_type.ESP_ZB_ZCL_ATTR_TYPE_U64, "S8": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S8, "S16": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S16, "S24": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S24, "S32": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S32, "S40": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S40, "S48": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S48, "S56": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S56, "S64": attr_type.ESP_ZB_ZCL_ATTR_TYPE_S64, "8BIT_ENUM": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BIT_ENUM, "16BIT_ENUM": attr_type.ESP_ZB_ZCL_ATTR_TYPE_16BIT_ENUM, "SEMI": attr_type.ESP_ZB_ZCL_ATTR_TYPE_SEMI, "SINGLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_SINGLE, "DOUBLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_DOUBLE, "OCTET_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_OCTET_STRING, "CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING, "LONG_OCTET_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_LONG_OCTET_STRING, "LONG_CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_LONG_CHAR_STRING, "ARRAY": attr_type.ESP_ZB_ZCL_ATTR_TYPE_ARRAY, "16BIT_ARRAY": attr_type.ESP_ZB_ZCL_ATTR_TYPE_16BIT_ARRAY, "32BIT_ARRAY": attr_type.ESP_ZB_ZCL_ATTR_TYPE_32BIT_ARRAY, "STRUCTURE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_STRUCTURE, "SET": attr_type.ESP_ZB_ZCL_ATTR_TYPE_SET, "BAG": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BAG, "TIME_OF_DAY": attr_type.ESP_ZB_ZCL_ATTR_TYPE_TIME_OF_DAY, "DATE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_DATE, "UTC_TIME": attr_type.ESP_ZB_ZCL_ATTR_TYPE_UTC_TIME, "CLUSTER_ID": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CLUSTER_ID, "ATTRIBUTE_ID": attr_type.ESP_ZB_ZCL_ATTR_TYPE_ATTRIBUTE_ID, "BACNET_OID": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BACNET_OID, "IEEE_ADDR": attr_type.ESP_ZB_ZCL_ATTR_TYPE_IEEE_ADDR, "128_BIT_KEY": attr_type.ESP_ZB_ZCL_ATTR_TYPE_128_BIT_KEY, "INVALID": attr_type.ESP_ZB_ZCL_ATTR_TYPE_INVALID, } ATTR_ACCESS = { "READ_ONLY": 1, "WRITE_ONLY": 2, "READ_WRITE": 3, } ================================================ FILE: components/zigbee/zigbee_ep.py ================================================ import copy from esphome.components import light, output import esphome.config_validation as cv from esphome.const import ( CONF_COMPONENTS, CONF_DEVICE, CONF_DEVICE_CLASS, CONF_ID, CONF_MAX_LENGTH, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLUME_FLOW_RATE, UNIT_AMPERE, UNIT_CELSIUS, UNIT_HECTOPASCAL, UNIT_HERTZ, UNIT_KILOWATT, UNIT_KILOWATT_HOURS, UNIT_LUX, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_OHM, UNIT_PARTS_PER_MILLION, UNIT_PASCAL, UNIT_PERCENT, UNIT_SECOND, UNIT_VOLT, UNIT_WATT, ) from esphome.core import CORE, ID from .const import ( CONF_ACCESS, CONF_AS_GENERIC, CONF_ATTRIBUTE_ID, CONF_ATTRIBUTES, CONF_CLUSTERS, CONF_DEVICE_TYPE, CONF_ENDPOINTS, CONF_NUM, CONF_REPORT, CONF_ROLE, CONF_SCALE, AnalogInputType, BacnetUnit, BinarySensor, Sensor, Switch, ) from .types import ZigBeeAttribute # endpoint configs: ep_configs = { "binary_input": { CONF_DEVICE_TYPE: "CUSTOM_ATTR", CONF_CLUSTERS: [ { CONF_ID: "BINARY_INPUT", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x55, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, { CONF_ATTRIBUTE_ID: 0x51, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x6F, CONF_VALUE: 0, CONF_ACCESS: 0, CONF_TYPE: "8BITMAP", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x1C, CONF_ACCESS: 0, CONF_TYPE: "CHAR_STRING", CONF_REPORT: False, CONF_SCALE: 1, }, ], }, ], }, "analog_input": { CONF_DEVICE_TYPE: "CUSTOM_ATTR", CONF_CLUSTERS: [ { CONF_ID: "ANALOG_INPUT", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x55, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "SINGLE", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, { CONF_ATTRIBUTE_ID: 0x51, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x6F, CONF_VALUE: 0, CONF_ACCESS: 0, CONF_TYPE: "8BITMAP", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x1C, CONF_ACCESS: 0, CONF_TYPE: "CHAR_STRING", CONF_REPORT: False, CONF_SCALE: 1, }, ], }, ], }, "binary_output": { CONF_DEVICE_TYPE: "CUSTOM_ATTR", CONF_CLUSTERS: [ { CONF_ID: "BINARY_OUTPUT", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x55, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, { CONF_ATTRIBUTE_ID: 0x51, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x6F, CONF_VALUE: 0, CONF_ACCESS: 0, CONF_TYPE: "8BITMAP", CONF_REPORT: False, CONF_SCALE: 1, }, { CONF_ATTRIBUTE_ID: 0x1C, CONF_ACCESS: 0, CONF_TYPE: "CHAR_STRING", CONF_REPORT: False, CONF_SCALE: 1, }, ], }, ], }, "temperature": { CONF_DEVICE_TYPE: "TEMPERATURE_SENSOR", CONF_CLUSTERS: [ { CONF_ID: "TEMP_MEASUREMENT", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: 0, CONF_ACCESS: 0, CONF_TYPE: "S16", CONF_REPORT: True, CONF_SCALE: 100, CONF_DEVICE: None, }, ], }, ], }, "on_off": { CONF_DEVICE_TYPE: "ON_OFF_OUTPUT", CONF_CLUSTERS: [ { CONF_ID: "ON_OFF", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, ], }, "on_off_light": { CONF_DEVICE_TYPE: "ON_OFF_LIGHT", CONF_CLUSTERS: [ { CONF_ID: "ON_OFF", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, ], }, "color_light": { CONF_DEVICE_TYPE: "COLOR_DIMMABLE_LIGHT", CONF_CLUSTERS: [ { CONF_ID: "ON_OFF", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, { CONF_ID: "LEVEL_CONTROL", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: 255, CONF_ACCESS: 0, CONF_TYPE: "U8", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, { CONF_ID: "COLOR_CONTROL", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x3, CONF_VALUE: 0x616B, CONF_ACCESS: 0, CONF_TYPE: "U16", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, { CONF_ATTRIBUTE_ID: 0x4, CONF_VALUE: 0x607D, CONF_ACCESS: 0, CONF_TYPE: "U16", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, { CONF_ATTRIBUTE_ID: 0x400A, CONF_VALUE: 8, CONF_ACCESS: 0, CONF_TYPE: "16BITMAP", CONF_REPORT: False, CONF_SCALE: 1, }, ], }, ], }, "level_light": { CONF_DEVICE_TYPE: "DIMMABLE_LIGHT", CONF_CLUSTERS: [ { CONF_ID: "ON_OFF", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: False, CONF_ACCESS: 0, CONF_TYPE: "BOOL", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, { CONF_ID: "LEVEL_CONTROL", CONF_ROLE: "SERVER", CONF_ATTRIBUTES: [ { CONF_ATTRIBUTE_ID: 0x0, CONF_VALUE: 255, CONF_ACCESS: 0, CONF_TYPE: "U8", CONF_REPORT: True, CONF_SCALE: 1, CONF_DEVICE: None, }, ], }, ], }, } def get_next_ep_num(eps): try: ep_num = [i for i in range(1, 240) if i not in eps][0] eps.append(ep_num) except IndexError as e: raise cv.Invalid( "Too many devices. Zigbee can define only 240 endpoints." ) from e return ep_num ANALOG_INPUT_APPTYPE = { (DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS): AnalogInputType.Temp_Degrees_C, (DEVICE_CLASS_HUMIDITY, UNIT_PERCENT): AnalogInputType.Relative_Humidity_Percent, (DEVICE_CLASS_PRESSURE, UNIT_PASCAL): AnalogInputType.Pressure_Pascal, (DEVICE_CLASS_VOLUME_FLOW_RATE, "L/s"): AnalogInputType.Flow_Liters_Per_Sec, (DEVICE_CLASS_CURRENT, UNIT_AMPERE): AnalogInputType.Current_Amps, (DEVICE_CLASS_FREQUENCY, UNIT_HERTZ): AnalogInputType.Frequency_Hz, (DEVICE_CLASS_POWER, UNIT_WATT): AnalogInputType.Power_Watts, (DEVICE_CLASS_POWER, UNIT_KILOWATT): AnalogInputType.Power_Kilo_Watts, (DEVICE_CLASS_ENERGY, UNIT_KILOWATT_HOURS): AnalogInputType.Energy_Kilo_Watt_Hours, (DEVICE_CLASS_DURATION, UNIT_SECOND): AnalogInputType.Time_Seconds, } """ not implemented: AnalogInputType.Percentage AnalogInputType.Parts_Per_Million AnalogInputType.Rotational_Speed_RPM AnalogInputType.Count AnalogInputType.Enthalpy_KJoules_Per_Kg """ BACNET_UNIT = { UNIT_CELSIUS: BacnetUnit.DEGREES_CELSIUS, "°F": BacnetUnit.DEGREES_FAHRENHEIT, UNIT_PERCENT: BacnetUnit.PERCENT, UNIT_LUX: BacnetUnit.LUXES, UNIT_VOLT: BacnetUnit.VOLTS, UNIT_MICROGRAMS_PER_CUBIC_METER: BacnetUnit.MICROGRAMS_PER_CUBIC_METER, UNIT_PARTS_PER_MILLION: BacnetUnit.PARTS_PER_MILLION, UNIT_OHM: BacnetUnit.OHMS, UNIT_HECTOPASCAL: BacnetUnit.HECTOPASCALS, } def create_device_ep(eps, dev, generic=False): ep = {} ep[CONF_NUM] = get_next_ep_num(eps) if dev["id"].type.inherits_from(Sensor): if dev.get(CONF_DEVICE_CLASS, "") in ep_configs and not generic: ep.update(copy.deepcopy(ep_configs[dev[CONF_DEVICE_CLASS]])) else: # get application type from device class and meas unit # if none get BACNET unit from meas unit ep.update(copy.deepcopy(ep_configs["analog_input"])) dev_class = dev.get(CONF_DEVICE_CLASS) unit = dev.get(CONF_UNIT_OF_MEASUREMENT) apptype = ANALOG_INPUT_APPTYPE.get((dev_class, unit)) bacunit = BACNET_UNIT.get(unit) if apptype is not None: ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append( { CONF_ATTRIBUTE_ID: 0x100, CONF_VALUE: (apptype << 16) + 0xFFFF, CONF_ACCESS: 0, CONF_TYPE: "U32", CONF_REPORT: False, CONF_SCALE: 1, }, ) if bacunit is not None: ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append( { CONF_ATTRIBUTE_ID: 0x75, CONF_VALUE: bacunit, CONF_ACCESS: 0, CONF_TYPE: "16BIT_ENUM", CONF_REPORT: False, CONF_SCALE: 1, }, ) elif dev["id"].type.inherits_from(Switch): if generic: ep.update(copy.deepcopy(ep_configs["binary_output"])) else: ep.update(copy.deepcopy(ep_configs["on_off"])) elif dev["id"].type.inherits_from(BinarySensor): ep.update(copy.deepcopy(ep_configs["binary_input"])) elif dev["id"].type.inherits_from(light.LightState): # NB: output.FloatOutput is a subclass of output.BinaryOutput thus must be checked first if ( dev["platform"] in ["monochromatic"] or "dimmer" in dev["platform"] or ( "output" in dev and dev["output"].type.inherits_from(output.FloatOutput) ) ): if generic: ep.update(copy.deepcopy(ep_configs["analog_output"])) ep[CONF_CLUSTERS].append( copy.deepcopy(ep_configs["binary_output"][CONF_CLUSTERS]) ) else: ep.update(copy.deepcopy(ep_configs["level_light"])) elif dev["platform"] in ["binary", "status_led"] or ( "output" in dev and dev["output"].type.inherits_from(output.BinaryOutput) ): if generic: ep.update(copy.deepcopy(ep_configs["binary_output"])) else: ep.update(copy.deepcopy(ep_configs["on_off_light"])) else: ep.update(copy.deepcopy(ep_configs["color_light"])) for cl in ep.get(CONF_CLUSTERS, []): for attr in cl[CONF_ATTRIBUTES]: if ( attr[CONF_ATTRIBUTE_ID] == 0x1C and CONF_VALUE not in attr and "name" in dev ): # set name name = dev["name"].encode("ascii", "ignore").decode() # use unidecode attr[CONF_VALUE] = str(name) attr[CONF_MAX_LENGTH] = len(str(name)) if CONF_DEVICE in attr: # connect device attr[CONF_DEVICE] = dev["id"] # create attribute ID id = ID(None, is_declaration=True, type=ZigBeeAttribute) id.resolve(CORE.component_ids) CORE.component_ids.add(id.id) attr[CONF_ID] = id else: attr[CONF_ID] = None return ep def get_device_entries(conf: list, component_type): devices = [] for d in conf: if CONF_ID in d and d[CONF_ID].type.inherits_from(component_type): devices.append(d) else: for dd in d.values(): if isinstance(dd, dict): devices.extend(get_device_entries([dd], component_type)) return devices def create_ep(config, full_conf): eps = [] comp_ids = len(CORE.component_ids) if CONF_ENDPOINTS in config: eps = [ep.get(CONF_NUM) for ep in config[CONF_ENDPOINTS]] for ep in config[CONF_ENDPOINTS]: if CONF_NUM not in ep: ep[CONF_NUM] = get_next_ep_num(eps) ep_list = config.get(CONF_ENDPOINTS, []) if CONF_COMPONENTS in config: devs = [ i["id"] for i in get_device_entries(full_conf.get("light", []), light.LightState) + get_device_entries(full_conf.get("switch", []), Switch) + get_device_entries(full_conf.get("sensor", []), Sensor) + get_device_entries(full_conf.get("binary_sensor", []), BinarySensor) ] add_devices = [] if isinstance(config[CONF_COMPONENTS], list): list_devs = [] for dev in config[CONF_COMPONENTS]: if dev in devs: list_devs.append(dev) add_devices = [ i for i in get_device_entries( full_conf.get("light", []), light.LightState ) + get_device_entries(full_conf.get("switch", []), Switch) + get_device_entries(full_conf.get("sensor", []), Sensor) + get_device_entries(full_conf.get("binary_sensor", []), BinarySensor) if i["id"] in list_devs ] if config[CONF_COMPONENTS] == "all": add_devices = [ i for i in get_device_entries( full_conf.get("light", []), light.LightState ) + get_device_entries(full_conf.get("switch", []), Switch) + get_device_entries(full_conf.get("sensor", []), Sensor) + get_device_entries(full_conf.get("binary_sensor", []), BinarySensor) if ("name" in i) and not i.get("internal") ] for dev in add_devices: ep_list.append(create_device_ep(eps, dev, config[CONF_AS_GENERIC])) if not ep_list: ep_list = [ { CONF_DEVICE_TYPE: "CUSTOM_ATTR", CONF_NUM: 1, } ] return ep_list, len(CORE.component_ids) - comp_ids ================================================ FILE: components/zigbee/zigbee_helpers.c ================================================ #include "ha/esp_zigbee_ha_standard.h" #include "zigbee_helpers.h" esp_zb_cluster_list_t *esphome_zb_default_clusters_create(esp_zb_ha_standard_devices_t device_type) { esp_zb_cluster_list_t *cluster_list; switch (device_type) { case ESP_ZB_HA_ON_OFF_SWITCH_DEVICE_ID: { esp_zb_on_off_switch_cfg_t config = ESP_ZB_DEFAULT_ON_OFF_SWITCH_CONFIG(); cluster_list = esp_zb_on_off_switch_clusters_create(&config); break; } case ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID: { esp_zb_on_off_light_cfg_t config = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG(); cluster_list = esp_zb_on_off_light_clusters_create(&config); break; } case ESP_ZB_HA_COLOR_DIMMER_SWITCH_DEVICE_ID: { esp_zb_color_dimmable_switch_cfg_t config = ESP_ZB_DEFAULT_COLOR_DIMMABLE_SWITCH_CONFIG(); cluster_list = esp_zb_color_dimmable_switch_clusters_create(&config); break; } case ESP_ZB_HA_COLOR_DIMMABLE_LIGHT_DEVICE_ID: { esp_zb_color_dimmable_light_cfg_t config = ESP_ZB_DEFAULT_COLOR_DIMMABLE_LIGHT_CONFIG(); cluster_list = esp_zb_color_dimmable_light_clusters_create(&config); break; } case ESP_ZB_HA_MAINS_POWER_OUTLET_DEVICE_ID: { esp_zb_mains_power_outlet_cfg_t config = ESP_ZB_DEFAULT_MAINS_POWER_OUTLET_CONFIG(); cluster_list = esp_zb_mains_power_outlet_clusters_create(&config); break; } case ESP_ZB_HA_SHADE_DEVICE_ID: { esp_zb_shade_cfg_t config = ESP_ZB_DEFAULT_SHADE_CONFIG(); cluster_list = esp_zb_shade_clusters_create(&config); break; } case ESP_ZB_HA_SHADE_CONTROLLER_DEVICE_ID: { esp_zb_shade_controller_cfg_t config = ESP_ZB_DEFAULT_SHADE_CONTROLLER_CONFIG(); cluster_list = esp_zb_shade_controller_clusters_create(&config); break; } case ESP_ZB_HA_DOOR_LOCK_DEVICE_ID: { esp_zb_door_lock_cfg_t config = ESP_ZB_DEFAULT_DOOR_LOCK_CONFIG(); cluster_list = esp_zb_door_lock_clusters_create(&config); break; } case ESP_ZB_HA_DOOR_LOCK_CONTROLLER_DEVICE_ID: { esp_zb_door_lock_controller_cfg_t config = ESP_ZB_DEFAULT_DOOR_LOCK_CONTROLLER_CONFIG(); cluster_list = esp_zb_door_lock_controller_clusters_create(&config); break; } case ESP_ZB_HA_TEMPERATURE_SENSOR_DEVICE_ID: { esp_zb_temperature_sensor_cfg_t config = ESP_ZB_DEFAULT_TEMPERATURE_SENSOR_CONFIG(); cluster_list = esp_zb_temperature_sensor_clusters_create(&config); break; } case ESP_ZB_HA_CONFIGURATION_TOOL_DEVICE_ID: { esp_zb_configuration_tool_cfg_t config = ESP_ZB_DEFAULT_CONFIGURATION_TOOL_CONFIG(); cluster_list = esp_zb_configuration_tool_clusters_create(&config); break; } case ESP_ZB_HA_THERMOSTAT_DEVICE_ID: { esp_zb_thermostat_cfg_t config = ESP_ZB_DEFAULT_THERMOSTAT_CONFIG(); cluster_list = esp_zb_thermostat_clusters_create(&config); break; } case ESP_ZB_HA_WINDOW_COVERING_DEVICE_ID: { esp_zb_window_covering_cfg_t config = ESP_ZB_DEFAULT_WINDOW_COVERING_CONFIG(); cluster_list = esp_zb_window_covering_clusters_create(&config); break; } case ESP_ZB_HA_WINDOW_COVERING_CONTROLLER_DEVICE_ID: { esp_zb_window_covering_controller_cfg_t config = ESP_ZB_DEFAULT_WINDOW_COVERING_CONTROLLER_CONFIG(); cluster_list = esp_zb_window_covering_controller_clusters_create(&config); break; } default: // create empty cluster list; cluster_list = esp_zb_zcl_cluster_list_create(); } return cluster_list; } esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, void *value_p) { esp_err_t ret; ret = esp_zb_cluster_update_attr(attr_list, attr_id, value_p); if (ret != ESP_OK) { ESP_LOGE("zigbee_helper", "Ignore previous attribute not found error"); if (attr_access > 0) { ret = esp_zb_cluster_add_attr(attr_list, cluster_id, attr_id, attr_type, attr_access, value_p); } else { ret = esphome_zb_cluster_add_attr(cluster_id, attr_list, attr_id, value_p); } } return ret; } esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask) { esp_err_t ret; ret = esp_zb_cluster_list_update_cluster(cluster_list, attr_list, cluster_id, role_mask); if (ret != ESP_OK) { ESP_LOGE("zigbee_helper", "Ignore previous cluster not found error"); switch (cluster_id) { case ESP_ZB_ZCL_CLUSTER_ID_BASIC: ret = esp_zb_cluster_list_add_basic_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG: ret = esp_zb_cluster_list_add_power_config_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG: ret = esp_zb_cluster_list_add_device_temp_config_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: ret = esp_zb_cluster_list_add_identify_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_GROUPS: ret = esp_zb_cluster_list_add_groups_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_SCENES: ret = esp_zb_cluster_list_add_scenes_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF: ret = esp_zb_cluster_list_add_on_off_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG: ret = esp_zb_cluster_list_add_on_off_switch_config_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL: ret = esp_zb_cluster_list_add_level_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ALARMS: ret = esp_zb_cluster_list_add_alarms_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_TIME: ret = esp_zb_cluster_list_add_time_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: ret = esp_zb_cluster_list_add_analog_input_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT: ret = esp_zb_cluster_list_add_analog_output_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE: ret = esp_zb_cluster_list_add_analog_value_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: ret = esp_zb_cluster_list_add_binary_input_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT: ret = esp_zb_cluster_list_add_binary_output_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE: ret = esp_zb_cluster_list_add_binary_value_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT: ret = esp_zb_cluster_list_add_multistate_input_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT: ret = esp_zb_cluster_list_add_multistate_output_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE: ret = esp_zb_cluster_list_add_multistate_value_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING: ret = esp_zb_cluster_list_add_commissioning_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE: ret = esp_zb_cluster_list_add_ota_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL: ret = esp_zb_cluster_list_add_poll_control_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG: ret = esp_zb_cluster_list_add_shade_config_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK: ret = esp_zb_cluster_list_add_door_lock_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING: ret = esp_zb_cluster_list_add_window_covering_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT: ret = esp_zb_cluster_list_add_thermostat_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL: ret = esp_zb_cluster_list_add_fan_control_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_DEHUMIDIFICATION_CONTROL: ret = esp_zb_cluster_list_add_dehumidification_control_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG: ret = esp_zb_cluster_list_add_thermostat_ui_config_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL: ret = esp_zb_cluster_list_add_color_control_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT: ret = esp_zb_cluster_list_add_illuminance_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT: ret = esp_zb_cluster_list_add_temperature_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT: ret = esp_zb_cluster_list_add_pressure_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT: ret = esp_zb_cluster_list_add_flow_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT: ret = esp_zb_cluster_list_add_humidity_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING: ret = esp_zb_cluster_list_add_occupancy_sensing_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT: ret = esp_zb_cluster_list_add_ph_measurement_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT: ret = esp_zb_cluster_list_add_ec_measurement_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT: ret = esp_zb_cluster_list_add_wind_speed_measurement_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT: ret = esp_zb_cluster_list_add_carbon_dioxide_measurement_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT: ret = esp_zb_cluster_list_add_pm2_5_measurement_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE: ret = esp_zb_cluster_list_add_ias_zone_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_IAS_ACE: ret = esp_zb_cluster_list_add_ias_ace_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_IAS_WD: ret = esp_zb_cluster_list_add_ias_wd_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_PRICE: ret = esp_zb_cluster_list_add_price_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_DRLC: ret = esp_zb_cluster_list_add_drlc_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_METERING: ret = esp_zb_cluster_list_add_metering_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION: ret = esp_zb_cluster_list_add_meter_identification_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT: ret = esp_zb_cluster_list_add_electrical_meas_cluster(cluster_list, attr_list, role_mask); break; case ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS: ret = esp_zb_cluster_list_add_diagnostics_cluster(cluster_list, attr_list, role_mask); break; default: ret = esp_zb_cluster_list_add_custom_cluster(cluster_list, attr_list, role_mask); } } return ret; } esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id) { switch (cluster_id) { case ESP_ZB_ZCL_CLUSTER_ID_BASIC: return esp_zb_basic_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG: return esp_zb_power_config_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG: return esp_zb_device_temp_config_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: return esp_zb_identify_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_GROUPS: return esp_zb_groups_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_SCENES: return esp_zb_scenes_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF: return esp_zb_on_off_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG: return esp_zb_on_off_switch_config_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL: return esp_zb_level_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ALARMS: return esp_zb_alarms_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_TIME: return esp_zb_time_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: return esp_zb_analog_input_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT: return esp_zb_analog_output_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE: return esp_zb_analog_value_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: return esp_zb_binary_input_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT: return esp_zb_binary_output_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE: return esp_zb_binary_value_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT: return esp_zb_multistate_input_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT: return esp_zb_multistate_output_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE: return esp_zb_multistate_value_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING: return esp_zb_commissioning_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE: return esp_zb_ota_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL: return esp_zb_poll_control_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG: return esp_zb_shade_config_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK: return esp_zb_door_lock_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING: return esp_zb_window_covering_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT: return esp_zb_thermostat_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL: return esp_zb_fan_control_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_DEHUMIDIFICATION_CONTROL: return esp_zb_dehumidification_control_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG: return esp_zb_thermostat_ui_config_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL: return esp_zb_color_control_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT: return esp_zb_illuminance_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT: return esp_zb_temperature_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT: return esp_zb_pressure_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT: return esp_zb_flow_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT: return esp_zb_humidity_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING: return esp_zb_occupancy_sensing_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT: return esp_zb_ph_measurement_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT: return esp_zb_ec_measurement_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT: return esp_zb_wind_speed_measurement_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT: return esp_zb_carbon_dioxide_measurement_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT: return esp_zb_pm2_5_measurement_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE: return esp_zb_ias_zone_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_IAS_ACE: return esp_zb_ias_ace_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_IAS_WD: return esp_zb_ias_wd_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_PRICE: return esp_zb_price_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_DRLC: return esp_zb_drlc_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_METERING: return esp_zb_metering_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION: return esp_zb_meter_identification_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT: return esp_zb_electrical_meas_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS: return esp_zb_diagnostics_cluster_create(NULL); default: return esp_zb_zcl_attr_list_create(cluster_id); } } esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p) { switch (cluster_id) { case ESP_ZB_ZCL_CLUSTER_ID_BASIC: return esp_zb_basic_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_POWER_CONFIG: return esp_zb_power_config_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_DEVICE_TEMP_CONFIG: return esp_zb_device_temp_config_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: return esp_zb_identify_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_GROUPS: return esp_zb_groups_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_SCENES: return esp_zb_scenes_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF: return esp_zb_on_off_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ON_OFF_SWITCH_CONFIG: return esp_zb_on_off_switch_config_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_LEVEL_CONTROL: return esp_zb_level_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ALARMS: return esp_zb_alarms_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_TIME: return esp_zb_time_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: return esp_zb_analog_input_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT: return esp_zb_analog_output_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_VALUE: return esp_zb_analog_value_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: return esp_zb_binary_input_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT: return esp_zb_binary_output_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_VALUE: return esp_zb_binary_value_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_INPUT: return esp_zb_multistate_input_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_OUTPUT: return esp_zb_multistate_output_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_MULTI_VALUE: return esp_zb_multistate_value_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_COMMISSIONING: return esp_zb_commissioning_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_OTA_UPGRADE: return esp_zb_ota_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_POLL_CONTROL: return esp_zb_poll_control_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_SHADE_CONFIG: return esp_zb_shade_config_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_DOOR_LOCK: return esp_zb_door_lock_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_WINDOW_COVERING: return esp_zb_window_covering_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT: return esp_zb_thermostat_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_FAN_CONTROL: return esp_zb_fan_control_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_DEHUMIDIFICATION_CONTROL: return esp_zb_dehumidification_control_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_THERMOSTAT_UI_CONFIG: return esp_zb_thermostat_ui_config_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_COLOR_CONTROL: return esp_zb_color_control_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT: return esp_zb_illuminance_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT: return esp_zb_temperature_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT: return esp_zb_pressure_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_FLOW_MEASUREMENT: return esp_zb_flow_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT: return esp_zb_humidity_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_OCCUPANCY_SENSING: return esp_zb_occupancy_sensing_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_PH_MEASUREMENT: return esp_zb_ph_measurement_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_EC_MEASUREMENT: return esp_zb_ec_measurement_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_WIND_SPEED_MEASUREMENT: return esp_zb_wind_speed_measurement_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_CARBON_DIOXIDE_MEASUREMENT: return esp_zb_carbon_dioxide_measurement_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_PM2_5_MEASUREMENT: return esp_zb_pm2_5_measurement_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_IAS_ZONE: return esp_zb_ias_zone_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_IAS_WD: return esp_zb_ias_wd_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_DRLC: return esp_zb_drlc_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_METER_IDENTIFICATION: return esp_zb_meter_identification_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_ELECTRICAL_MEASUREMENT: return esp_zb_electrical_meas_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_DIAGNOSTICS: return esp_zb_diagnostics_cluster_add_attr(attr_list, attr_id, value_p); default: return ESP_FAIL; } } ================================================ FILE: components/zigbee/zigbee_helpers.h ================================================ #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_zigbee_core.h" esp_zb_cluster_list_t *esphome_zb_default_clusters_create(esp_zb_ha_standard_devices_t device_type); esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list, esp_zb_attribute_list_t *attr_list, uint8_t role_mask); esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id); esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, void *value_p); esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, uint8_t attr_type, uint8_t attr_access, void *value_p); #ifdef __cplusplus } namespace esphome { namespace zigbee {} } // namespace esphome #endif ================================================ FILE: components/zigbee/zigbee_pre_build.py.script ================================================ from os import path import re def_file = "./src/esphome/core/defines.h" if path.exists(def_file): # open file and replace number of components in ESPHOME_COMPONENT_COUNT with number from ZB_ESPHOME_COMPONENT_COUNT with open(def_file, "rt", encoding="utf-8") as f: content = f.read() match = re.search(r"#define ZB_ESPHOME_COMPONENT_COUNT (\d+)", content) if match: zb_count = match.group(1) content = re.sub(r"#define ESPHOME_COMPONENT_COUNT \d+", f"#define ESPHOME_COMPONENT_COUNT {zb_count}", content) print(f"updated ESPHOME_COMPONENT_COUNT: {zb_count}") # remove ZB_ESPHOME_COMPONENT_COUNT line content = re.sub(r"#define ZB_ESPHOME_COMPONENT_COUNT \d+\n", "", content) else: print("ZB_ESPHOME_COMPONENT_COUNT not found") with open(def_file, "w", encoding="utf-8") as f: f.write(content) ================================================ FILE: example_aht10_esp32h2.yaml ================================================ esphome: name: zb-sensor external_components: - source: components components: [zigbee] esp32: board: esp32-h2-devkitm-1 #flash_size: 4MB partitions: partitions_zb.csv framework: type: esp-idf #sdkconfig_options: #CONFIG_ESPTOOLPY_FLASHSIZE_4MB: y # Enable logging logger: hardware_uart: UART0 globals: - id: color_x type: float restore_value: no initial_value: '0' - id: color_y type: float restore_value: no initial_value: '0' i2c: sda: 12 scl: 22 scan: false sensor: - platform: aht10 variant: AHT10 temperature: name: "Living Room Temperature" id: "temp" filters: - delta: 0.1 humidity: name: "Living Room Humidity" id: "hum" filters: - delta: 1 on_value: then: - zigbee.setAttr: id: hum_attr value: !lambda "return x*100;" update_interval: 60s zigbee: id: "zb" endpoints: - num: 1 device_type: COLOR_DIMMABLE_LIGHT clusters: - id: ON_OFF attributes: - attribute_id: 0 type: bool on_value: then: - light.control: id: light_1 state: !lambda "return x;" - id: LEVEL_CONTROL attributes: - attribute_id: 0 type: U8 value: 255 on_value: then: - light.control: id: light_1 brightness: !lambda "return ((float)x)/255;" - id: COLOR_CONTROL attributes: - attribute_id: 3 type: U16 on_value: then: - lambda: id(color_x) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - attribute_id: 4 type: U16 on_value: then: - lambda: id(color_y) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - device_type: TEMPERATURE_SENSOR num: 2 clusters: - id: REL_HUMIDITY_MEASUREMENT attributes: - attribute_id: 0 id: hum_attr type: U16 report: true value: 200 - id: TEMP_MEASUREMENT attributes: - attribute_id: 0x0 type: S16 report: true value: 100 device: temp scale: 100 on_join: then: - logger.log: "Joined network" light: - platform: esp32_rmt_led_strip rgb_order: GRB pin: 8 num_leds: 1 #rmt_channel: 0 id: light_1 chipset: ws2812 #bit0_high: 100ns # rmt clk freq seems to be different on H2 #bit0_low: 300ns #bit1_high: 300ns #bit1_low: 100ns binary_sensor: - platform: gpio pin: number: 9 mode: input: true pullup: true inverted: true id: button_1 on_press: then: - zigbee.report: zb on_click: min_length: 5s max_length: 20s then: - zigbee.reset: zb ================================================ FILE: example_esp32c6.yaml ================================================ esphome: name: zb-example-c6 external_components: - source: components components: [zigbee] esp32: board: esp32-c6-devkitc-1 #flash_size: 4MB partitions: partitions_zb.csv framework: type: esp-idf #sdkconfig_options: #CONFIG_ESPTOOLPY_FLASHSIZE_4MB: y # Enable logging logger: hardware_uart: UART0 globals: - id: color_x type: float restore_value: no initial_value: '0' - id: color_y type: float restore_value: no initial_value: '0' sensor: - platform: internal_temperature name: "Internal Temperature" id: "temp" filters: - delta: 0.1 zigbee: id: "zb" endpoints: - num: 1 device_type: COLOR_DIMMABLE_LIGHT clusters: - id: ON_OFF attributes: - attribute_id: 0 type: bool on_value: then: - light.control: id: light_1 state: !lambda "return (bool)x;" - id: LEVEL_CONTROL attributes: - attribute_id: 0 type: U8 value: 255 on_value: then: - light.control: id: light_1 brightness: !lambda "return ((float)x)/255;" - id: COLOR_CONTROL attributes: - attribute_id: 3 type: U16 on_value: then: - lambda: id(color_x) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - attribute_id: 4 type: U16 on_value: then: - lambda: id(color_y) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - device_type: TEMPERATURE_SENSOR num: 2 clusters: - id: TEMP_MEASUREMENT attributes: - attribute_id: 0x0 type: S16 report: true value: 100 device: temp scale: 100 on_join: then: - logger.log: "Joined network" light: - platform: esp32_rmt_led_strip rgb_order: GRB pin: 8 num_leds: 1 #rmt_channel: 0 chipset: ws2812 id: light_1 binary_sensor: - platform: gpio pin: number: 9 mode: input: true pullup: true inverted: true id: button_1 on_press: then: - zigbee.report: zb on_click: min_length: 5s max_length: 20s then: - zigbee.reset: zb ================================================ FILE: example_esp32h2.yaml ================================================ esphome: name: zb-example-h2 external_components: - source: components components: [zigbee] esp32: board: esp32-h2-devkitm-1 #flash_size: 4MB partitions: partitions_zb.csv framework: type: esp-idf #sdkconfig_options: #CONFIG_ESPTOOLPY_FLASHSIZE_4MB: y # Enable logging logger: hardware_uart: UART0 globals: - id: color_x type: float restore_value: no initial_value: '0' - id: color_y type: float restore_value: no initial_value: '0' sensor: - platform: internal_temperature name: "Internal Temperature" id: "temp" filters: - delta: 0.1 zigbee: id: "zb" endpoints: - num: 1 device_type: COLOR_DIMMABLE_LIGHT clusters: - id: ON_OFF attributes: - attribute_id: 0 type: bool on_value: then: - light.control: id: light_1 state: !lambda "return x;" - id: LEVEL_CONTROL attributes: - attribute_id: 0 type: U8 value: 255 on_value: then: - light.control: id: light_1 brightness: !lambda "return ((float)x)/255;" - id: COLOR_CONTROL attributes: - attribute_id: 3 type: U16 on_value: then: - lambda: id(color_x) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - attribute_id: 4 type: U16 on_value: then: - lambda: id(color_y) = (float)x/65536; - light.control: id: light_1 red: !lambda "return zigbee::get_r_from_xy(id(color_x), id(color_y));" green: !lambda "return zigbee::get_g_from_xy(id(color_x), id(color_y));" blue: !lambda "return zigbee::get_b_from_xy(id(color_x), id(color_y));" - device_type: TEMPERATURE_SENSOR num: 2 clusters: - id: TEMP_MEASUREMENT attributes: - attribute_id: 0x0 type: S16 report: true value: 100 device: temp scale: 100 on_join: then: - logger.log: "Joined network" light: - platform: esp32_rmt_led_strip rgb_order: GRB pin: 8 num_leds: 1 #rmt_channel: 0 id: light_1 chipset: ws2812 #bit0_high: 100ns # rmt clk freq seems to be different on H2. Fixed in esphome 2025.02 #bit0_low: 300ns #bit1_high: 300ns #bit1_low: 100ns binary_sensor: - platform: gpio pin: number: 9 mode: input: true pullup: true inverted: true id: button_1 on_press: then: - zigbee.report: zb on_click: min_length: 5s max_length: 20s then: - zigbee.reset: zb ================================================ FILE: example_esp32h2_basic.yaml ================================================ esphome: name: zb-example-h2 external_components: - source: components components: [zigbee] esp32: board: esp32-h2-devkitm-1 partitions: partitions_zb.csv framework: type: esp-idf # Enable logging logger: hardware_uart: UART0 sensor: - platform: internal_temperature name: "Internal Temperature" id: "temp" filters: - delta: 0.1 zigbee: id: "zb" components: all on_join: then: - logger.log: "Joined network" light: - platform: esp32_rmt_led_strip rgb_order: GRB pin: 8 num_leds: 1 id: light_1 name: "my_light" chipset: ws2812 binary_sensor: - platform: gpio pin: number: 9 mode: input: true pullup: true inverted: true id: button_1 name: "button1" #on_press: # then: # - zigbee.report: zb on_click: min_length: 5s max_length: 20s then: - zigbee.reset: zb ================================================ FILE: example_time.yml ================================================ esphome: name: timesync-demo external_components: - source: components components: [zigbee] esp32: board: esp32-c6-devkitc-1 variant: esp32c6 flash_size: 8MB partitions: partitions_zb.csv framework: type: esp-idf sdkconfig_options: CONFIG_ESPTOOLPY_FLASHSIZE_8MB: y logger: zigbee: id: "zb" endpoints: # We need at least one endpoint defined - device_type: TEMPERATURE_SENSOR num: 1 clusters: - id: TEMP_MEASUREMENT attributes: - attribute_id: 0x0 type: S16 report: true id: zb_temp value: 0 on_join: then: - logger.log: "Joined network" time: - platform: zigbee id: the_time timezone: Europe/London on_time_sync: then: - logger.log: "Synchronized system clock" on_time: - seconds: /10 then: - logger.log: "Tick-tock, every 10 seconds" ================================================ FILE: partitions_zb.csv ================================================ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, app0, app, ota_0, , 0x1B0000, app1, app, ota_1, , 0x1B0000, nvs, data, nvs, , 0x6D000, zb_storage, data, fat, , 16K, zb_fct, data, fat, , 1K, ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools==69.2.0", "wheel~=0.43.0"] build-backend = "setuptools.build_meta" [project] name = "esphome" license = {text = "MIT"} description = "Make creating custom firmwares for ESP32/ESP8266 super easy." readme = "README.md" authors = [ {name = "The ESPHome Authors", email = "esphome@nabucasa.com"} ] keywords = ["home", "automation"] classifiers = [ "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Programming Language :: C++", "Programming Language :: Python :: 3", "Topic :: Home Automation", ] requires-python = ">=3.9.0" dynamic = ["dependencies", "optional-dependencies", "version"] [project.urls] "Documentation" = "https://esphome.io" "Source Code" = "https://github.com/esphome/esphome" "Bug Tracker" = "https://github.com/esphome/issues/issues" "Feature Request Tracker" = "https://github.com/esphome/feature-requests/issues" "Discord" = "https://discord.gg/KhAMKrd" "Forum" = "https://community.home-assistant.io/c/esphome" "Twitter" = "https://twitter.com/esphome_" [project.scripts] esphome = "esphome.__main__:main" [tool.setuptools] platforms = ["any"] zip-safe = false include-package-data = true [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} optional-dependencies.dev = { file = ["requirements_dev.txt"] } optional-dependencies.test = { file = ["requirements_test.txt"] } optional-dependencies.displays = { file = ["requirements_optional.txt"] } version = {attr = "esphome.const.__version__"} [tool.setuptools.packages.find] include = ["esphome*"] [tool.black] target-version = ["py39", "py310"] exclude = 'generated' [tool.pytest.ini_options] testpaths = [ "tests", ] addopts = [ "--cov=esphome", "--cov-branch", ] [tool.pylint.MAIN] py-version = "3.9" ignore = [ "api_pb2.py", ] persistent = false [tool.pylint.REPORTS] score = false [tool.pylint."MESSAGES CONTROL"] disable = [ "format", "missing-docstring", "fixme", "unused-argument", "global-statement", "too-few-public-methods", "too-many-lines", "too-many-locals", "too-many-ancestors", "too-many-branches", "too-many-statements", "too-many-arguments", "too-many-return-statements", "too-many-instance-attributes", "duplicate-code", "invalid-name", "cyclic-import", "redefined-builtin", "undefined-loop-variable", "useless-object-inheritance", "stop-iteration-return", "import-outside-toplevel", # Broken "unsupported-membership-test", "unsubscriptable-object", ] [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.ruff] required-version = ">=0.5.0" [tool.ruff.lint] select = [ "E", # pycodestyle "F", # pyflakes/autoflake "I", # isort "PL", # pylint "UP", # pyupgrade ] ignore = [ "E501", # line too long "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target ] [tool.ruff.lint.isort] force-sort-within-sections = true known-first-party = [ "esphome", ] combine-as-imports = true split-on-trailing-comma = false