diff --git a/components/wp_core/__init__.py b/components/wp_core/__init__.py index 742b3c0..5b39c8a 100644 --- a/components/wp_core/__init__.py +++ b/components/wp_core/__init__.py @@ -8,6 +8,7 @@ def to_code(config): cg.add_global(cg.RawStatement('#include "esphome/components/wp_core/property.h"')) cg.add_global(cg.RawStatement('#include "esphome/components/wp_core/simple_variant.h"')) cg.add_global(cg.RawStatement('#include "esphome/components/wp_core/type.h"')) + cg.add_global(cg.RawStatement('#include "esphome/components/wp_core/unmapped_tracker.h"')) cg.add_global(cg.RawStatement('#include "esphome/components/wp_core/util.h"')) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(cg.Component) }) diff --git a/components/wp_core/mapper.cpp b/components/wp_core/mapper.cpp index 1c4b0d7..8ca72ea 100644 --- a/components/wp_core/mapper.cpp +++ b/components/wp_core/mapper.cpp @@ -88,6 +88,9 @@ Mapper::Mapper() { #elif defined(WPL_13) || defined(WPL_17) || defined(WPL_23) betriebsartMap = {{0x00, "Notbetrieb"}, {0x01, "Bereitschaft"}, {0x02, "Programm"}, {0x03, "Komfort"}, {0x04, "Eco"}, {0x05, "Warmwasser"}}; +#elif defined(WPL_33) + betriebsartMap = {{0x0000, "Notbetrieb"}, {0x0100, "Bereitschaft"}, {0x0200, "Automatik"}, + {0x0300, "Tagbetrieb"}, {0x0400, "Absenkbetrieb"}, {0x0500, "Warmwasser"}}; #else betriebsartMap = {{0x0000, "Notbetrieb"}, {0x0100, "Bereitschaft"}, {0x0300, "Tagbetrieb"}, {0x0400, "Absenkbetrieb"}, {0x0500, "Warmwasser"}, {0x0B00, "Automatik"}, diff --git a/components/wp_core/property.h b/components/wp_core/property.h index 51a7e9a..13d2389 100644 --- a/components/wp_core/property.h +++ b/components/wp_core/property.h @@ -479,6 +479,70 @@ struct Property : public oe32trta::detail::Property { PROPERTY(LAUFZEIT_VD_WW, 0x0802); PROPERTY(LAUFZEIT_VD_ABTAUEN, 0x0808); #endif + +#if defined(WPL_33) + PROPERTY(EINSTELL_SPEICHERSOLLTEMP, 0x0013, Type::et_dec_val); + PROPERTY(KESSELSOLLTEMP, 0x0002, Type::et_dec_val); + PROPERTY(RAUMSOLLTEMP_TAG, 0x0005, Type::et_dec_val); + PROPERTY(RAUMSOLLTEMP_NACHT, 0x0008, Type::et_dec_val); + PROPERTY(VORLAUFISTTEMP, 0x000f, Type::et_dec_val); + PROPERTY(VERDAMPFERTEMP, 0x0014, Type::et_dec_val); + PROPERTY(RUECKLAUFISTTEMP, 0x0016, Type::et_dec_val); + PROPERTY(PROGRAMMSCHALTER, 0x0112, Type::et_betriebsart); + PROPERTY(MAX_HYSTERESE, 0x0023, Type::et_little_endian); + PROPERTY(ANFAHRENT, 0x005d, Type::et_little_endian); + PROPERTY(SPEICHER_STATUS, 0x005a, Type::et_little_endian); + PROPERTY(SPEICHERBEDARF, 0x005f, Type::et_little_endian); + PROPERTY(SCHALTFKT_IWS, 0x0060); + PROPERTY(ABTAUUNGAKTIV, 0x0061, Type::et_little_endian); + PROPERTY(WAERMEPUMPEN_STATUS, 0x0062, Type::et_little_endian); + + // TEILVORRANG_WW, EVU_SPERRE_AKTIV, WW_ECO: proper bool types -- wired up as binary_sensors + // via templates/wp_binary_property.yaml (uses value.get()), not templates/wp_generic.yaml + // (which always does value.get() and would throw bad_variant_access/crash on a + // bool-holding variant). + PROPERTY(TEILVORRANG_WW, 0x005e, Type::et_little_bool); + PROPERTY(EVU_SPERRE_AKTIV, 0x0074, Type::et_bool); + PROPERTY(WW_ECO, 0x027e, Type::et_bool); + + PROPERTY(PUFFERSOLLTEMPERATUR, 0x01d5, Type::et_dec_val); + PROPERTY(HILFSKESSELSOLL, 0x01d7, Type::et_dec_val); + PROPERTY(LAUFZEIT_WP1, 0x01c4); + PROPERTY(LAUFZEIT_WP2, 0x01c5); + PROPERTY(LAUFZEIT_2WE, 0x01cb); + PROPERTY(STILLSTANDZEIT_0, 0x01cc); + PROPERTY(STILLSTANDZEIT_1, 0x01cd); + PROPERTY(MAXVORLAUFTEMP, 0x01e8, Type::et_dec_val); + PROPERTY(EINSTELL_SPEICHERSOLLTEMP2, 0x0a06, Type::et_dec_val); + PROPERTY(VERDICHTER, 0x07a8, Type::et_dec_val); + + // RAUMISTTEMP, RAUMFEUCHTE: not polled by wpl33.yaml itself (confirmed dead as a read via + // the FET CAN node), but core.yaml's Home Assistant Sensors block unconditionally writes + // HA's own room temperature/humidity to CanNode::HK1 using these -- needed by every device + // family. + PROPERTY(RAUMISTTEMP, 0x4ec7, Type::et_dec_val); + PROPERTY(RAUMFEUCHTE, 0x4ec8, Type::et_dec_val); + + PROPERTY(SOMMERBETRIEB, 0xfdb4, Type::et_little_bool); + PROPERTY(STILLSTANDZEIT, 0xfdb1, Type::et_little_endian); + PROPERTY(WP_STATUS, 0xfdae, Type::et_little_endian); + + // GERAETEKONFIGURATION, ACCESS_EEPROM, INITIALISIERUNG, BUSKONFIGURATION: broadcast + // unsolicited by the unit on multiple CAN nodes -- never queried, see + // templates/wp_generic_passive.yaml. + PROPERTY(GERAETEKONFIGURATION, 0x0010); + PROPERTY(ACCESS_EEPROM, 0x0030, Type::et_little_endian); + PROPERTY(INITIALISIERUNG, 0x00fe, Type::et_little_endian); + PROPERTY(BUSKONFIGURATION, 0x00fd); + + // GERAETE_ID: also broadcast unsolicited (Heizmodul) -- string-valued (major-minor device + // id), see templates/wp_text_passive.yaml. + PROPERTY(GERAETE_ID, 0x000b, Type::et_dev_id); + + PROPERTY(MAXRUECKLAUFTEMP, 0x0028, Type::et_dec_val); + PROPERTY(BIVALENZTEMPERATUR_HZG, 0x01ac, Type::et_dec_val); + PROPERTY(ANLAGEFROST, 0x0a00, Type::et_dec_val); +#endif }; #endif diff --git a/components/wp_core/unmapped_tracker.h b/components/wp_core/unmapped_tracker.h new file mode 100644 index 0000000..cf1e017 --- /dev/null +++ b/components/wp_core/unmapped_tracker.h @@ -0,0 +1,98 @@ +#if !defined(UNMAPPED_TRACKER_H) +#define UNMAPPED_TRACKER_H + +#include +#include +#include +#include +#include + +#include "esphome.h" +#include "property.h" + +// Tracks CAN property ids that arrive on the bus but have no entry in property.h (shown as +// "UNKNOWN" by Mapper::getProperty). Meant purely as a discovery aid for extending property.h -- +// disabled by default (see core.yaml's track_unmapped_properties substitution) since it writes to +// flash every time a genuinely new id is seen. +class UnmappedPropertyTracker { + public: + static constexpr std::size_t kCapacity{24U}; + + struct Entry { + char canNode[12]{}; + std::uint16_t propertyId{0xFFFFU}; + }; + + struct State { + std::array entries{}; + std::uint8_t nextIndex{0U}; + }; + + static UnmappedPropertyTracker& instance() { + static UnmappedPropertyTracker t; + return t; + } + + void track(const std::string_view canNode, const std::uint16_t propertyId) { + for (const auto& entry : state_.entries) { + if (entry.propertyId == propertyId && canNode == entry.canNode) { + return; // already recorded + } + } + auto& slot = state_.entries[state_.nextIndex]; + std::snprintf(slot.canNode, sizeof(slot.canNode), "%.*s", static_cast(canNode.size()), canNode.data()); + slot.propertyId = propertyId; + state_.nextIndex = static_cast((state_.nextIndex + 1U) % kCapacity); + pref_.save(&state_); + ESP_LOGI("UnmappedPropertyTracker", "New unmapped property seen: %.*s (0x%04x)", + static_cast(canNode.size()), canNode.data(), propertyId); + } + + std::string getSummary() const { + std::string result; + for (const auto& entry : state_.entries) { + if (entry.propertyId == 0xFFFFU) { + continue; + } + if (!result.empty()) { + result += ", "; + } + static_assert(sizeof(entry.canNode) == 12U, "adjust the %.11s precision below if this changes"); + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.11s (0x%04x)", entry.canNode, entry.propertyId); + result += buf; + } + return result.empty() ? "none" : result; + } + + private: + UnmappedPropertyTracker() { + pref_ = esphome::global_preferences->make_preference(esphome::fnv1_hash("wp_unmapped_tracker_v1")); + if (!pref_.load(&state_)) { + state_ = State{}; + return; + } + prune(); + } + + // Drops entries for property ids that have since been added to property.h -- the flash + // state is an append-only log of "first seen as unmapped", not a live view, so without this + // fixed properties would keep showing up here forever. + void prune() { + bool changed{false}; + for (auto& entry : state_.entries) { + if (entry.propertyId != 0xFFFFU && Property{entry.propertyId}.name != "UNKNOWN") { + entry = Entry{}; + changed = true; + } + } + if (changed) { + pref_.save(&state_); + } + } + + esphome::ESPPreferenceObject pref_; + State state_{}; +}; + +#endif diff --git a/components/wp_core/util.cpp b/components/wp_core/util.cpp index cefdd7d..9bbc21a 100644 --- a/components/wp_core/util.cpp +++ b/components/wp_core/util.cpp @@ -12,14 +12,19 @@ void syncTime(esphome::time::RealTimeClock* time_obj) { } ESP_LOGI("TIMESYNC", "Synchronizing heat pump time to %d.%d.%d %d:%d", time.day_of_month, time.month, time.year, time.hour, time.minute); +#if defined(WPL_33) + const auto* target = CanNode::Manager; +#else + const auto* target = CanNode::Kessel; +#endif #if !defined(THZ_304) - queueTransmission(CanNode::Kessel, Property::kSEKUNDE, toggleEndianness(time.second)); + queueTransmission(target, Property::kSEKUNDE, toggleEndianness(time.second)); #endif - queueTransmission(CanNode::Kessel, Property::kMINUTE, toggleEndianness(time.minute)); - queueTransmission(CanNode::Kessel, Property::kSTUNDE, toggleEndianness(time.hour)); - queueTransmission(CanNode::Kessel, Property::kTAG, toggleEndianness(time.day_of_month)); - queueTransmission(CanNode::Kessel, Property::kMONAT, toggleEndianness(time.month)); - queueTransmission(CanNode::Kessel, Property::kJAHR, toggleEndianness(time.year - 2000U)); + queueTransmission(target, Property::kMINUTE, toggleEndianness(time.minute)); + queueTransmission(target, Property::kSTUNDE, toggleEndianness(time.hour)); + queueTransmission(target, Property::kTAG, toggleEndianness(time.day_of_month)); + queueTransmission(target, Property::kMONAT, toggleEndianness(time.month)); + queueTransmission(target, Property::kJAHR, toggleEndianness(time.year - 2000U)); } std::unordered_set& getWhitelistedEntities() { diff --git a/esp32-poe-technik.yaml b/esp32-poe-technik.yaml index 8534288..789f165 100644 --- a/esp32-poe-technik.yaml +++ b/esp32-poe-technik.yaml @@ -125,6 +125,7 @@ packages: # wpl13: !include OneESP32ToRuleThemAll/yaml/wpl13.yaml # wpl17: !include OneESP32ToRuleThemAll/yaml/wpl17.yaml # wpl23: !include OneESP32ToRuleThemAll/yaml/wpl23.yaml +# wpl33: !include OneESP32ToRuleThemAll/yaml/wpl33.yaml # energymeter: !include OneESP32ToRuleThemAll/yaml/features/energymeter.yaml # evu: !include OneESP32ToRuleThemAll/yaml/features/evu.yaml # watermeter: !include OneESP32ToRuleThemAll/yaml/features/watermeter.yaml diff --git a/yaml/core.yaml b/yaml/core.yaml index 97a2b0d..6335119 100644 --- a/yaml/core.yaml +++ b/yaml/core.yaml @@ -4,12 +4,11 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-std=gnu++2a" - - "-DESPCLIENT_ID=$espclient_can_id" - build_unflags: - - "-std=gnu++11" + # build_unflags ("-std=gnu++11", "-fno-rtti") dropped -- ignored under the native ESP-IDF + # toolchain anyway; simple_variant.h no longer relies on RTTI either. + build_flags: + - "-std=gnu++2a" + - "-DESPCLIENT_ID=$espclient_can_id" esp32: framework: @@ -30,7 +29,12 @@ substitutions: interval_slow: 5min interval_very_slow: 15min interval_once_in_a_while: 1h + interval_once_a_day: 24h espclient_can_id: 0x6a2 + # Logs+persists (via flash) CAN property ids that arrive but have no property.h entry -- a + # discovery aid for extending property.h. Off by default since it writes to flash on every + # newly-seen id; override per-device to "true" to enable. + track_unmapped_properties: "false" ######################################### # # @@ -88,6 +92,25 @@ sensor: queueTransmission(CanNode::HK1, Property::kRAUMFEUCHTE, static_cast(x * 10.0f)); } +######################################### +# # +# Diagnostics # +# # +######################################### +text_sensor: + - platform: template + name: Unmapped Properties + id: unmapped_properties + icon: "mdi:help-network" + entity_category: diagnostic + update_interval: $interval_once_in_a_while + lambda: |- + if constexpr(${track_unmapped_properties}) { + return UnmappedPropertyTracker::instance().getSummary(); + } else { + return std::string{"disabled"}; + } + ######################################### # # # Buttons # @@ -154,6 +177,11 @@ canbus: const auto [property, value] = processCanMessage(x); const auto* canNode = getCanNodeByCanId(can_id); if(canNode != nullptr) { + if constexpr(${track_unmapped_properties}) { + if(property.name == "UNKNOWN") { + UnmappedPropertyTracker::instance().track(canNode->name, property.id); + } + } auto callback = CallbackHandler::instance().getCallback(std::make_pair(canNode,property)); callback(value); } else { diff --git a/yaml/features/analytics.yaml b/yaml/features/analytics.yaml index b7caf8b..c89ff5b 100644 --- a/yaml/features/analytics.yaml +++ b/yaml/features/analytics.yaml @@ -21,9 +21,8 @@ esphome: std::string info_payload = id(heat_pump_variant) + "," + ${district} + "," + "${version_hash}"; id(analytics_mqtt).publish("analytics/v1/" + id(global_device_id).state + "/info", info_payload, 0, true); - script.execute: check_for_update - platformio_options: - build_flags: - - "-DANALYTICS" + build_flags: + - "-DANALYTICS" substitutions: version_hash: !include ../../version diff --git a/yaml/templates/wp_binary_property.yaml b/yaml/templates/wp_binary_property.yaml new file mode 100644 index 0000000..f28bfc8 --- /dev/null +++ b/yaml/templates/wp_binary_property.yaml @@ -0,0 +1,43 @@ +defaults: + target: "Kessel" + update_interval: "1h" + name_suffix: "" + exposed: "true" + +binary_sensor: + - platform: template + name: ${property}${name_suffix} + id: ${property}${name_suffix} + +esphome: + on_boot: + - priority: -50 + then: + - lambda: |- + CallbackHandler::instance().addCallback({CanNode::${target},Property::k${property}},[](const SimpleVariant& value){ + const auto newValue{value.get()}; + auto property{id(${property}${name_suffix})}; + if(!property->has_state() || property->state != newValue) { + property->publish_state(newValue); + } + }); + + #if defined(ANALYTICS) + if(${exposed}) { + getWhitelistedEntities().insert(id(${property}${name_suffix})); + } + #endif + - priority: -100.0 + then: + - delay: 250ms + - lambda: |- + queueRequest(CanNode::${target}, Property::k${property}); + +# binary_sensor (unlike sensor) has no update_interval+lambda self-polling, so re-request +# periodically via a plain interval instead -- same approach the project's old wp_binary_sensor.yaml +# template used before the component restructure. +interval: + - interval: ${update_interval} + then: + - lambda: |- + queueRequest(CanNode::${target}, Property::k${property}); diff --git a/yaml/templates/wp_datetime.yaml b/yaml/templates/wp_datetime.yaml index 4d35f1f..3ca4bed 100644 --- a/yaml/templates/wp_datetime.yaml +++ b/yaml/templates/wp_datetime.yaml @@ -1,3 +1,6 @@ +defaults: + target: "Kessel" + esphome: on_boot: - priority: -50 @@ -19,28 +22,28 @@ esphome: // publish the value id(HEATPUMP_DATETIME).state = std::string(buf); }; - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kJAHR},[updateTime](const SimpleVariant& value){ + CallbackHandler::instance().addCallback({CanNode::${target},Property::kJAHR},[updateTime](const SimpleVariant& value){ updateTime([&value](ESPTime& timeToSet){ timeToSet.year = 2000U + static_cast(value); }); }); - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kMONAT},[updateTime](const SimpleVariant& value){ + CallbackHandler::instance().addCallback({CanNode::${target},Property::kMONAT},[updateTime](const SimpleVariant& value){ updateTime([&value](ESPTime& timeToSet){ timeToSet.month = static_cast(value); }); }); - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kWOCHENTAG},[](const SimpleVariant&){}); - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kTAG},[updateTime](const SimpleVariant& value){ + CallbackHandler::instance().addCallback({CanNode::${target},Property::kWOCHENTAG},[](const SimpleVariant&){}); + CallbackHandler::instance().addCallback({CanNode::${target},Property::kTAG},[updateTime](const SimpleVariant& value){ updateTime([&value](ESPTime& timeToSet){ timeToSet.day_of_month = static_cast(value); }); }); - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kSTUNDE},[updateTime](const SimpleVariant& value){ + CallbackHandler::instance().addCallback({CanNode::${target},Property::kSTUNDE},[updateTime](const SimpleVariant& value){ updateTime([&value](ESPTime& timeToSet){ timeToSet.hour = static_cast(value); }); }); - CallbackHandler::instance().addCallback({CanNode::Kessel,Property::kMINUTE},[updateTime](const SimpleVariant& value){ + CallbackHandler::instance().addCallback({CanNode::${target},Property::kMINUTE},[updateTime](const SimpleVariant& value){ updateTime([&value](ESPTime& timeToSet){ timeToSet.minute = static_cast(value); }); @@ -50,11 +53,11 @@ esphome: then: - delay: 250ms - lambda: |- - queueRequest(CanNode::Kessel, Property::kJAHR); - queueRequest(CanNode::Kessel, Property::kMONAT); - queueRequest(CanNode::Kessel, Property::kTAG); - queueRequest(CanNode::Kessel, Property::kSTUNDE); - queueRequest(CanNode::Kessel, Property::kMINUTE); + queueRequest(CanNode::${target}, Property::kJAHR); + queueRequest(CanNode::${target}, Property::kMONAT); + queueRequest(CanNode::${target}, Property::kTAG); + queueRequest(CanNode::${target}, Property::kSTUNDE); + queueRequest(CanNode::${target}, Property::kMINUTE); text_sensor: - platform: template diff --git a/yaml/templates/wp_generic_passive.yaml b/yaml/templates/wp_generic_passive.yaml new file mode 100644 index 0000000..34c3f62 --- /dev/null +++ b/yaml/templates/wp_generic_passive.yaml @@ -0,0 +1,40 @@ +defaults: + icon: "mdi:blank" + accuracy_decimals: "0" + target: "Kessel" + unit: "" + scaler: "1.0" + name_suffix: "" + exposed: "true" + state_class: "measurement" + +# For properties the heat pump broadcasts unsolicited (never in response to a request) -- +# registers a passive receive-only callback, unlike wp_generic.yaml this never calls +# queueRequest (no self-poll, no one-shot request at boot). +sensor: + - platform: template + name: ${property}${name_suffix} + id: ${property}${name_suffix} + icon: ${icon} + unit_of_measurement: ${unit} + accuracy_decimals: ${accuracy_decimals} + state_class: ${state_class} + +esphome: + on_boot: + - priority: -50 + then: + - lambda: |- + CallbackHandler::instance().addCallback({CanNode::${target},Property::k${property}},[](const SimpleVariant& value){ + const auto newValue{${scaler} * value.get()}; + auto property{id(${property}${name_suffix})}; + if(!std::is_eq(property->state <=> newValue)) { + property->publish_state(newValue); + } + }); + + #if defined(ANALYTICS) + if(${exposed}) { + getWhitelistedEntities().insert(id(${property}${name_suffix})); + } + #endif diff --git a/yaml/templates/wp_text.yaml b/yaml/templates/wp_text.yaml new file mode 100644 index 0000000..f796778 --- /dev/null +++ b/yaml/templates/wp_text.yaml @@ -0,0 +1,37 @@ +defaults: + icon: "mdi:blank" + target: "Kessel" + update_interval: "never" + name_suffix: "" + exposed: "true" + +text_sensor: + - platform: template + name: ${property}${name_suffix} + id: ${property}${name_suffix} + icon: ${icon} + update_interval: ${update_interval} + +esphome: + on_boot: + - priority: -50 + then: + - lambda: |- + CallbackHandler::instance().addCallback({CanNode::${target},Property::k${property}},[](const SimpleVariant& value){ + const auto newValue{value.get()}; + auto property{id(${property}${name_suffix})}; + if(property->state != newValue) { + property->publish_state(newValue); + } + }); + + #if defined(ANALYTICS) + if(${exposed}) { + getWhitelistedEntities().insert(id(${property}${name_suffix})); + } + #endif + - priority: -100.0 + then: + - delay: 250ms + - lambda: |- + queueRequest(CanNode::${target}, Property::k${property}); diff --git a/yaml/templates/wp_text_passive.yaml b/yaml/templates/wp_text_passive.yaml new file mode 100644 index 0000000..308b583 --- /dev/null +++ b/yaml/templates/wp_text_passive.yaml @@ -0,0 +1,33 @@ +defaults: + icon: "mdi:blank" + target: "Kessel" + name_suffix: "" + exposed: "true" + +# For string-valued properties the heat pump broadcasts unsolicited (never in response to a +# request) -- registers a passive receive-only callback, unlike wp_text.yaml this never calls +# queueRequest (no one-shot request at boot). +text_sensor: + - platform: template + name: ${property}${name_suffix} + id: ${property}${name_suffix} + icon: ${icon} + +esphome: + on_boot: + - priority: -50 + then: + - lambda: |- + CallbackHandler::instance().addCallback({CanNode::${target},Property::k${property}},[](const SimpleVariant& value){ + const auto newValue{value.get()}; + auto property{id(${property}${name_suffix})}; + if(property->state != newValue) { + property->publish_state(newValue); + } + }); + + #if defined(ANALYTICS) + if(${exposed}) { + getWhitelistedEntities().insert(id(${property}${name_suffix})); + } + #endif diff --git a/yaml/thz304.yaml b/yaml/thz304.yaml index 43cadb1..9db521b 100644 --- a/yaml/thz304.yaml +++ b/yaml/thz304.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DTHZ_304" + build_flags: + - "-DTHZ_304" on_boot: - priority: -50 then: diff --git a/yaml/thz404.yaml b/yaml/thz404.yaml index 4f6686a..b71afe4 100644 --- a/yaml/thz404.yaml +++ b/yaml/thz404.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DTHZ_404" + build_flags: + - "-DTHZ_404" ######################################### # # diff --git a/yaml/thz504.yaml b/yaml/thz504.yaml index 4c9d22e..a12662d 100644 --- a/yaml/thz504.yaml +++ b/yaml/thz504.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DTHZ_504" + build_flags: + - "-DTHZ_504" ######################################### # # diff --git a/yaml/thz5_5_eco.yaml b/yaml/thz5_5_eco.yaml index 0381a35..92a12a9 100644 --- a/yaml/thz5_5_eco.yaml +++ b/yaml/thz5_5_eco.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DTHZ_5_5_ECO" + build_flags: + - "-DTHZ_5_5_ECO" ######################################### # # diff --git a/yaml/ttf07.yaml b/yaml/ttf07.yaml index 68b9e9e..c0b0fae 100644 --- a/yaml/ttf07.yaml +++ b/yaml/ttf07.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DTTF_07_C" + build_flags: + - "-DTTF_07_C" on_boot: - priority: -50 then: diff --git a/yaml/wpl13.yaml b/yaml/wpl13.yaml index 4d13686..78c0dcc 100644 --- a/yaml/wpl13.yaml +++ b/yaml/wpl13.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DWPL_13" + build_flags: + - "-DWPL_13" on_boot: - priority: -50 then: @@ -56,3 +55,9 @@ packages: WAERMEQUELLENPUMPE: !include { file: templates/wp_binary.yaml, vars: { name: "WAERMEQUELLENPUMPE" }} PASSIVKUEHLVENTIL_HEIZEN: !include { file: templates/wp_binary.yaml, vars: { name: "PASSIVKUEHLVENTIL_HEIZEN" }} PASSIVKUEHLVENTIL_KUEHLEN: !include { file: templates/wp_binary.yaml, vars: { name: "PASSIVKUEHLVENTIL_KUEHLEN" }} + + PROGRAMMSCHALTER: !include { file: templates/wp_select.yaml, vars: { property: PROGRAMMSCHALTER, target: "Manager", mapper_func_set: getBetriebsartId, mapper_func_get: getBetriebsart, options: ["Notbetrieb", "Bereitschaft", "Programm", "Komfort", "Eco", "Unbekannt"] }} + + VORLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} + VORLAUFSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFSOLLTEMP", target: "HK1" }} + RUECKLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} diff --git a/yaml/wpl17.yaml b/yaml/wpl17.yaml index cfdc8df..32f5ef1 100644 --- a/yaml/wpl17.yaml +++ b/yaml/wpl17.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DWPL_17" + build_flags: + - "-DWPL_17" ######################################### # # @@ -142,3 +141,9 @@ packages: FESTWERTBETRIEB: !include { file: templates/wp_temperature.yaml, vars: { property: "FESTWERTBETRIEB", target: "Kessel" }} WW_SOLLTEMPERATUR: !include { file: templates/wp_temperature.yaml, vars: { property: "WW_SOLLTEMPERATUR", target: "Kessel" }} GRENZE_KUEHLEN: !include { file: templates/wp_temperature.yaml, vars: { property: "GRENZE_KUEHLEN", target: "Kessel" }} + + PROGRAMMSCHALTER: !include { file: templates/wp_select.yaml, vars: { property: PROGRAMMSCHALTER, target: "Manager", mapper_func_set: getBetriebsartId, mapper_func_get: getBetriebsart, options: ["Notbetrieb", "Bereitschaft", "Programm", "Komfort", "Eco", "Unbekannt"] }} + + VORLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} + VORLAUFSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFSOLLTEMP", target: "HK1" }} + RUECKLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} diff --git a/yaml/wpl23.yaml b/yaml/wpl23.yaml index eb64b94..1d84d13 100644 --- a/yaml/wpl23.yaml +++ b/yaml/wpl23.yaml @@ -4,9 +4,8 @@ # # ######################################### esphome: - platformio_options: - build_flags: - - "-DWPL_23" + build_flags: + - "-DWPL_23" ######################################### # # @@ -37,3 +36,9 @@ packages: LAUFZEIT_NHZ1_2: !include { file: templates/wp_generic.yaml, vars: { property: "LAUFZEIT_NHZ1_2", unit: "h", target: "Kessel" }} ZEIT_ABTAUEN: !include { file: templates/wp_generic.yaml, vars: { property: "ZEIT_ABTAUEN", unit: "min", target: "Heizmodul" }} STARTS_ABTAUEN: !include { file: templates/wp_generic.yaml, vars: { property: "STARTS_ABTAUEN", target: "Heizmodul" }} + + PROGRAMMSCHALTER: !include { file: templates/wp_select.yaml, vars: { property: PROGRAMMSCHALTER, target: "Manager", mapper_func_set: getBetriebsartId, mapper_func_get: getBetriebsart, options: ["Notbetrieb", "Bereitschaft", "Programm", "Komfort", "Eco", "Unbekannt"] }} + + VORLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} + VORLAUFSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFSOLLTEMP", target: "HK1" }} + RUECKLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFISTTEMP", update_interval: $interval_medium, target: "MFG" }} diff --git a/yaml/wpl33.yaml b/yaml/wpl33.yaml new file mode 100644 index 0000000..94de802 --- /dev/null +++ b/yaml/wpl33.yaml @@ -0,0 +1,159 @@ +######################################### +# # +# ESPHome # +# # +######################################### +esphome: + build_flags: + - "-DWPL_33" + on_boot: + - priority: -50 + then: + - lambda: |- + CallbackHandler::instance().addCallback(std::make_pair(CanNode::Manager,Property::kWAERMEPUMPEN_STATUS),[](const SimpleVariant& value){ + const std::bitset<9U> status_bits{static_cast(value)}; + id(VERDICHTER).publish_state(status_bits.test(0U)); + id(STOERUNG).publish_state(status_bits.test(4U)); + }); + - priority: -100 + then: + - lambda: |- + queueRequest(CanNode::Manager, Property::kWAERMEPUMPEN_STATUS); + +######################################### +# # +# Global variables # +# # +######################################### +globals: + - id: heat_pump_variant + type: std::string + initial_value: '"WPL33"' + +######################################### +# # +# Substitutions # +# # +######################################### +substitutions: + # WPL33's datetime is read from/written to the Manager CAN node, not Kessel like the rest + # of the WPL family (see wpl_base.yaml). + datetime_target: "Manager" + # WPL33's CAN bus runs at 20kbps, unlike the 50kbps default wpl_base.yaml sets for WPL13/17/23. + can_bus_bit_rate: 20kbps + +######################################### +# # +# CAN Bus # +# # +######################################### +# WPL33 no longer inherits wpl_base.yaml wholesale (see below) -- it applies the bit rate +# override itself instead of relying on wpl_base.yaml to do it. +canbus: + - id: !extend wp_can + bit_rate: $can_bus_bit_rate + +text: + - id: !extend HK1_CAN_ID + initial_value: "0x601" + - id: !extend HK2_CAN_ID + initial_value: "0x602" + # WPL33's Heizmodul is on 0x500, unlike the 0x514 default wpl_base.yaml sets for WPL13/17/23. + - id: !extend Heizmodul_CAN_ID + initial_value: "0x500" + +######################################### +# # +# Packages # +# # +######################################### +# WPL33 does NOT include wpl_base.yaml as a whole -- unlike WPL13/17/23, a large fraction of +# its properties are confirmed (via live logs) to never return real data on this unit, and +# every time we found more it meant either duplicating exceptions across 3 sibling files or +# fighting !remove's inability to clean up baked on_boot lambda code. Instead this opts in +# explicitly to everything wpl_base.yaml provides, one section at a time, omitting only the +# properties already confirmed dead (below). If more turn out to be dead later, just delete +# their line here -- no other file is affected. +packages: + CORE: !include { file: core.yaml } + MANAGER: !include { file: templates/wp_can_id.yaml, vars: { can_node: "Manager" , can_id: "0x480" }} + + # wpl_base.yaml also decodes BETRIEBS_STATUS into 12 binary sensors (HEIZKREISPUMPE_1/2, + # PUFFERLADEPUMPE_1, QUELLENPUMPE, STOERAUSGANG, KUEHLBETRIEB, MISCHER_AUF/ZU_HEIZKREIS_2, + # NHZ_1/2/3, EVU_SPERRE). Omitted entirely: BETRIEBS_STATUS itself is confirmed dead on this + # unit (always returns the sentinel 32768), so none of those 12 would ever publish anything + # but "unknown". + + # Everything else wpl_base.yaml provides turned out to be dead on this unit too (confirmed + # via further live logs) -- only these few survive: + VERDAMPFERTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VERDAMPFERTEMP" , target: "Heizmodul" }} + MAXVORLAUFTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "MAXVORLAUFTEMP" , target: "Kessel" }} + MAXRUECKLAUFTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "MAXRUECKLAUFTEMP" , target: "Kessel" }} + AUSSENTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "AUSSENTEMP" , update_interval: $interval_medium , target: "Kessel" }} + ANLAGEFROST: !include { file: templates/wp_temperature.yaml, vars: { property: "ANLAGEFROST" , update_interval: $interval_medium , target: "Kessel" }} + BIVALENZTEMPERATUR_HZG: !include { file: templates/wp_temperature.yaml, vars: { property: "BIVALENZTEMPERATUR_HZG", target: "Kessel" }} + + HEATPUMP_DATETIME: !include { file: templates/wp_datetime.yaml, vars: { target: "$datetime_target" } } + + VERDICHTER: !include { file: templates/wp_binary.yaml, vars: { name: "VERDICHTER" }} + STOERUNG: !include { file: templates/wp_binary.yaml, vars: { name: "STOERUNG" }} + + # WPL33 uses different Betriebsart strings than the rest of the WPL family (see + # components/wp_core/mapper.cpp's WPL_33 betriebsartMap branch), so it declares its own + # PROGRAMMSCHALTER here instead of inheriting wpl_base.yaml's shared one. + PROGRAMMSCHALTER: !include { file: templates/wp_select.yaml, vars: { property: PROGRAMMSCHALTER, target: "Manager", mapper_func_set: getBetriebsartId, mapper_func_get: getBetriebsart, options: ["Automatik", "Warmwasser", "Bereitschaft", "Tagbetrieb", "Absenkbetrieb", "Notbetrieb", "Unbekannt"] }} + + SOMMERBETRIEB: !include { file: templates/wp_binary_property.yaml, vars: { property: "SOMMERBETRIEB", update_interval: $interval_very_slow, target: "Manager" }} + WW_ECO: !include { file: templates/wp_binary_property.yaml, vars: { property: "WW_ECO", update_interval: $interval_very_slow, target: "Manager" }} + + PUFFERSOLLTEMPERATUR: !include { file: templates/wp_temperature.yaml, vars: { property: "PUFFERSOLLTEMPERATUR", update_interval: $interval_medium }} + SPEICHERSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "SPEICHERSOLLTEMP", update_interval: $interval_medium }} + SPEICHERISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "SPEICHERISTTEMP", update_interval: $interval_medium }} + EINSTELL_SPEICHERSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "EINSTELL_SPEICHERSOLLTEMP", update_interval: $interval_once_a_day }} + EINSTELL_SPEICHERSOLLTEMP2: !include { file: templates/wp_temperature.yaml, vars: { property: "EINSTELL_SPEICHERSOLLTEMP2", update_interval: $interval_once_a_day }} + HILFSKESSELSOLL: !include { file: templates/wp_temperature.yaml, vars: { property: "HILFSKESSELSOLL", update_interval: $interval_once_in_a_while }} + KESSELSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "KESSELSOLLTEMP", update_interval: $interval_once_in_a_while }} + RAUMSOLLTEMP_TAG: !include { file: templates/wp_temperature.yaml, vars: { property: "RAUMSOLLTEMP_TAG", update_interval: $interval_once_a_day, target: "Manager" }} + RAUMSOLLTEMP_NACHT: !include { file: templates/wp_temperature.yaml, vars: { property: "RAUMSOLLTEMP_NACHT", update_interval: $interval_once_a_day, target: "Manager" }} + + # wpl_base.yaml assumes MFG/HK1/MFG for these three (matching WPL13/17/23), but this unit + # only responds on the targets below -- declared here instead of relying on the shared ones. + VORLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFISTTEMP", update_interval: $interval_medium, target: "HK1" }} + VORLAUFSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFSOLLTEMP", update_interval: $interval_medium }} + RUECKLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFISTTEMP", update_interval: $interval_medium, target: "Manager" }} + + TEILVORRANG_WW: !include { file: templates/wp_binary_property.yaml, vars: { property: "TEILVORRANG_WW", update_interval: $interval_medium, target: "Manager" }} + EVU_SPERRE_AKTIV: !include { file: templates/wp_binary_property.yaml, vars: { property: "EVU_SPERRE_AKTIV", target: "Manager" }} + SPEICHERBEDARF: !include { file: templates/wp_generic.yaml, vars: { property: "SPEICHERBEDARF", target: "Manager" }} + ANFAHRENT: !include { file: templates/wp_generic.yaml, vars: { property: "ANFAHRENT", target: "Manager", update_interval: $interval_medium }} + MAX_HYSTERESE: !include { file: templates/wp_generic.yaml, vars: { property: "MAX_HYSTERESE", target: "Kessel", accuracy_decimals: "0" }} + # BETRIEBS_STATUS: never returns real data on this unit -- confirmed via logs, always 32768. + SPEICHER_STATUS: !include { file: templates/wp_generic.yaml, vars: { property: "SPEICHER_STATUS", update_interval: $interval_medium, target: "Heizmodul" }} + ABTAUUNGAKTIV: !include { file: templates/wp_generic.yaml, vars: { property: "ABTAUUNGAKTIV", target: "Heizmodul", update_interval: $interval_medium }} + SCHALTFKT_IWS: !include { file: templates/wp_generic.yaml, vars: { property: "SCHALTFKT_IWS", target: "Heizmodul" }} + FEHLERMELDUNG: !include { file: templates/wp_generic.yaml, vars: { property: "FEHLERMELDUNG", target: "Manager", icon: "mdi:alert-circle" }} + FEHLERMELDUNG_HM: !include { file: templates/wp_generic.yaml, vars: { property: "FEHLERMELDUNG", name_suffix: "_HM", target: "Heizmodul", icon: "mdi:alert-circle" }} + STILLSTANDZEIT: !include { file: templates/wp_generic.yaml, vars: { property: "STILLSTANDZEIT", update_interval: "${interval_once_in_a_while}", unit: "min", target: "Manager" }} + STILLSTANDZEIT_0: !include { file: templates/wp_generic.yaml, vars: { property: "STILLSTANDZEIT_0", update_interval: "$interval_medium", unit: "s", target: "Manager" }} + STILLSTANDZEIT_1: !include { file: templates/wp_generic.yaml, vars: { property: "STILLSTANDZEIT_1", update_interval: "$interval_medium", unit: "s", target: "Manager" }} + LAUFZEIT_WP1: !include { file: templates/wp_generic.yaml, vars: { property: "LAUFZEIT_WP1", update_interval: "${interval_once_in_a_while}", unit: "h", target: "Manager" }} + LAUFZEIT_WP2: !include { file: templates/wp_generic.yaml, vars: { property: "LAUFZEIT_WP2", update_interval: "${interval_once_in_a_while}", unit: "h", target: "Manager" }} + LAUFZEIT_2WE: !include { file: templates/wp_generic.yaml, vars: { property: "LAUFZEIT_2WE", update_interval: "${interval_once_in_a_while}", unit: "h", target: "Manager" }} + WP_STATUS: !include { file: templates/wp_generic.yaml, vars: { property: "WP_STATUS", update_interval: "$interval_medium", target: "Manager" }} + + # Broadcast unsolicited by the unit (seen on the bus without us ever requesting them) -- + # passive receive-only, never queried. + GERAETEKONFIGURATION: !include { file: templates/wp_generic_passive.yaml, vars: { property: "GERAETEKONFIGURATION", target: "Manager" }} + ACCESS_EEPROM: !include { file: templates/wp_generic_passive.yaml, vars: { property: "ACCESS_EEPROM", target: "Manager" }} + INITIALISIERUNG_HK1: !include { file: templates/wp_generic_passive.yaml, vars: { property: "INITIALISIERUNG", name_suffix: "_HK1", target: "HK1" }} + INITIALISIERUNG_KESSEL: !include { file: templates/wp_generic_passive.yaml, vars: { property: "INITIALISIERUNG", name_suffix: "_KESSEL", target: "Kessel" }} + INITIALISIERUNG_MANAGER: !include { file: templates/wp_generic_passive.yaml, vars: { property: "INITIALISIERUNG", name_suffix: "_MANAGER", target: "Manager" }} + INITIALISIERUNG_HEIZMODUL: !include { file: templates/wp_generic_passive.yaml, vars: { property: "INITIALISIERUNG", name_suffix: "_HEIZMODUL", target: "Heizmodul" }} + BUSKONFIGURATION: !include { file: templates/wp_generic_passive.yaml, vars: { property: "BUSKONFIGURATION", target: "Heizmodul" }} + GERAETE_ID: !include { file: templates/wp_text_passive.yaml, vars: { property: "GERAETE_ID", target: "Heizmodul" }} + + # Disabled by default -- not wired up in the original draft, and this unit has no secondary + # heat generator ("2. Waermeerzeuger") so the WAERMEERTRAG_2WE_* energy sensors that used to + # be listed here were removed outright (not just commented) -- see property.h. + # VERDICHTER_RAW: !include { file: templates/wp_generic.yaml, vars: { property: "VERDICHTER", target: "Manager", accuracy_decimals: "1" }} + # WAERMEPUMPEN_STATUS_RAW: !include { file: templates/wp_generic.yaml, vars: { property: "WAERMEPUMPEN_STATUS", target: "Manager" }} diff --git a/yaml/wpl_base.yaml b/yaml/wpl_base.yaml index f0cfb15..2e5b2a0 100644 --- a/yaml/wpl_base.yaml +++ b/yaml/wpl_base.yaml @@ -5,6 +5,7 @@ ######################################### substitutions: can_bus_bit_rate: 50kbps + datetime_target: "Kessel" ######################################### # # @@ -81,10 +82,7 @@ packages: FROSTSCHUTZ: !include { file: templates/wp_temperature.yaml, vars: { property: "FROSTSCHUTZ" , target: "MFG" }} VORLAUFTEMP_QUELLE: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFTEMP_QUELLE" , target: "Heizmodul" }} RUECKLAUFTEMP_QUELLE: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFTEMP_QUELLE" , target: "Heizmodul" }} - VORLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFISTTEMP" , update_interval: $interval_medium, target: "MFG" }} - VORLAUFSOLLTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VORLAUFSOLLTEMP" , target: "HK1" }} VERDAMPFERTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "VERDAMPFERTEMP" , target: "Heizmodul" }} - RUECKLAUFISTTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "RUECKLAUFISTTEMP" , update_interval: $interval_medium , target: "MFG" }} MAXVORLAUFTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "MAXVORLAUFTEMP" , target: "Kessel" }} MAXRUECKLAUFTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "MAXRUECKLAUFTEMP" , target: "Kessel" }} AUSSENTEMP: !include { file: templates/wp_temperature.yaml, vars: { property: "AUSSENTEMP" , update_interval: $interval_medium , target: "Kessel" }} @@ -154,9 +152,8 @@ packages: WAERMEERTRAG_WW_TAG_SUMME_KWH: !include { file: templates/wp_daily_energy_combined.yaml, vars: { sensor_name: "WAERMEERTRAG_WW_TAG_SUMME_KWH" , property_wh: "WAERMEERTRAG_WW_TAG_WH" , property_kwh: "WAERMEERTRAG_WW_TAG_KWH" , target: "Heizmodul" }} WAERMEERTRAG_HEIZ_TAG_SUMME_KWH: !include { file: templates/wp_daily_energy_combined.yaml, vars: { sensor_name: "WAERMEERTRAG_HEIZ_TAG_SUMME_KWH" , property_wh: "WAERMEERTRAG_HEIZ_TAG_WH" , property_kwh: "WAERMEERTRAG_HEIZ_TAG_KWH" , target: "Heizmodul" }} - HEATPUMP_DATETIME: !include { file: templates/wp_datetime.yaml } + HEATPUMP_DATETIME: !include { file: templates/wp_datetime.yaml, vars: { target: "$datetime_target" } } - PROGRAMMSCHALTER: !include { file: templates/wp_select.yaml, vars: { property: PROGRAMMSCHALTER, target: "Manager", mapper_func_set: getBetriebsartId, mapper_func_get: getBetriebsart, options: ["Notbetrieb", "Bereitschaft", "Programm", "Komfort", "Eco", "Unbekannt"] }} text: - id: !extend HK1_CAN_ID