Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/wp_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) })
3 changes: 3 additions & 0 deletions components/wp_core/mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
64 changes: 64 additions & 0 deletions components/wp_core/property.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>()), not templates/wp_generic.yaml
// (which always does value.get<float>() 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
98 changes: 98 additions & 0 deletions components/wp_core/unmapped_tracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#if !defined(UNMAPPED_TRACKER_H)
#define UNMAPPED_TRACKER_H

#include <array>
#include <cstdint>
#include <cstdio>
#include <string>
#include <string_view>

#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<Entry, kCapacity> 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<int>(canNode.size()), canNode.data());
slot.propertyId = propertyId;
state_.nextIndex = static_cast<std::uint8_t>((state_.nextIndex + 1U) % kCapacity);
pref_.save(&state_);
ESP_LOGI("UnmappedPropertyTracker", "New unmapped property seen: %.*s (0x%04x)",
static_cast<int>(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<State>(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
17 changes: 11 additions & 6 deletions components/wp_core/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<esphome::EntityBase*>& getWhitelistedEntities() {
Expand Down
1 change: 1 addition & 0 deletions esp32-poe-technik.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 34 additions & 6 deletions yaml/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"

#########################################
# #
Expand Down Expand Up @@ -88,6 +92,25 @@ sensor:
queueTransmission(CanNode::HK1, Property::kRAUMFEUCHTE, static_cast<std::uint16_t>(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 #
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions yaml/features/analytics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions yaml/templates/wp_binary_property.yaml
Original file line number Diff line number Diff line change
@@ -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<bool>()};
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});
Loading