diff --git a/README.md b/README.md index e69de29..c77a458 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,6 @@ +# ESPHome config/source/solar Folder + +This folder contains Contains source code files and related resources for solar projects. + + +For more information, visit the [ESPHome documentation](https://esphome.io/). \ No newline at end of file diff --git a/cbf_sthome.cpp b/cbf_sthome.cpp new file mode 100644 index 0000000..2d0e5dd --- /dev/null +++ b/cbf_sthome.cpp @@ -0,0 +1,342 @@ +#include "cbf_sthome.h" + +namespace solar +{ + // see common.h for definition of publish_spec_t + // publish_spec_t(int on_count, int interval, int timeout) + const publish_spec_t cbf_sthome::publish_spec = publish_spec_t(3, 15, 30); // default publish spec + const publish_spec_t cbf_sthome::rtr_publish_spec = publish_spec_t(2, 10, 40); // for remote transmission requests + + const std::string cbf_sthome::heartbeat = "HELLO\0\0\0"; // must be exactly 8 bytes including null terminators + + //cbf_sthome::cbf_sthome(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr) + // : cb_frame(msg_id, can_id, frame, rtr) + // { + // } + cbf_sthome& cbf_sthome::operator=(cbf_sthome&& src) + { + if (this != &src) { + cb_frame::operator=(std::move(src)); + src.clear(); + //ESP_LOGI(tag("pylon MOASS").c_str(), "%-20s %s", "Assigned pylon", this->to_string().c_str()); + } + return *this; + } + void cbf_sthome::clear() + { + cb_frame::clear(); + } + + bool cbf_sthome::verify_heartbeat() const + { + const auto& x = this->frame; + std::string hb(reinterpret_cast(x.data()), x.size()); + hb.erase(std::find_if(hb.rbegin(), hb.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), hb.end()); + return hb == heartbeat; + } + // float cbf_sthome::_get_battery_charge_voltage_limit() const + // { + // const auto& x = this->frame; + // return 0.1 * (float)((x[1] << 8) + x[0]); + // } + + // Function to build a message from the pylon canbus frame + std::string cbf_sthome::to_string() const + { + char buffer[128]; + const auto& x = this->frame; + switch (can_id) + { + case CB_CANBUS_ID01: + return rtr ? "Request for sthome-ut1 heartbeat" : verify_heartbeat() ? "sthome-ut1 alive" : ""; + case CB_CANBUS_ID02: + return rtr ? "Request for sthome-ut2 heartbeat" : verify_heartbeat() ? "sthome-ut2 alive" : ""; + case CB_CANBUS_ID03: + return rtr ? "Request for sthome-u3 heartbeat" : verify_heartbeat() ? "sthome-ut3 alive " : ""; + case CB_CANBUS_ID04: + return rtr ? "Request for sthome-ut4 heartbeat" : verify_heartbeat() ? "sthome-ut4 alive" : ""; + case CB_CANBUS_ID05: + return rtr ? "Request for sthome-ut5 heartbeat" : verify_heartbeat() ? "sthome-ut5 alive" : ""; + case CB_CANBUS_ID06: + return rtr ? "Request for sthome-ut6 heartbeat" : verify_heartbeat() ? "sthome-ut6 alive" : ""; + case CB_CANBUS_ID07: + return rtr ? "Request for sthome-ut7 heartbeat" : verify_heartbeat() ? "sthome-ut7 alive" : ""; + case CB_CANBUS_ID08: + return rtr ? "Request for sthome-ut8 heartbeat" : verify_heartbeat() ? "sthome-ut8 alive" : ""; + case CB_CANBUS_ID09: + return rtr ? "Request for sthome-ut9 heartbeat" : verify_heartbeat() ? "sthome-ut9 alive" : ""; + case CB_CANBUS_ID10: + return rtr ? "Request for sthome-ut10 heartbeat" : verify_heartbeat() ? "sthome-ut10 alive" : ""; + case CB_GEYSER_TEMPERATURE_TOP: + { + if(rtr) { + return "Request for geyser top temp"; + } + double temp_top = get_double(x[0], x[1], 256, true); + snprintf (buffer, sizeof(buffer), "GEYSER TOP: %.2f°C", temp_top); + return buffer; + } + case CB_GEYSER_TEMPERATURE_BOTTOM: + { + if(rtr) { + return "Request for geyser bottom temp"; + } + double temp_bot = get_double(x[0], x[1], 256, true); + snprintf (buffer, sizeof(buffer), "GEYSER BOT: %.2f°C", temp_bot); + return buffer; + } + case CB_GEYSER_TEMPERATURE_AMBIENT: + { + if(rtr) { + return "Request for geyser ambient temp"; + } + double temp_amb = get_double(x[0], x[1], 256, true); + snprintf (buffer, sizeof(buffer), "GEYSER AMB: %.2f°C", temp_amb); + return buffer; + } + case CB_GEYSER_HEATING: + { + if(rtr) { + return "Request for geyser heating"; + } + double heating_loss = get_double(x[0], x[1], 64, true); // -512W to 512W + double heat_gained = get_double(x[2], x[3], 128); // 0 to 512W + double calculated_heat_loss = get_double(x[4], x[5], 128); // 0 to 512W + unsigned int est_heating_time = (unsigned int) static_cast(256 * x[7] + x[6]); // 0 to 65536 seconds + snprintf (buffer, sizeof(buffer), "Heat: loss %.3fW, gain %.3fW, calc loss %.3fW, est. heat time %ds", heating_loss, heat_gained, calculated_heat_loss, est_heating_time); + return buffer; + } + case CB_GEYSER_ACTIVE_SCHEDULE: + { + if(rtr) { + return "Request for geyser schedule"; + } + double active_schedule_temp = get_double(x[0], x[1], 256, true); // -128 to 128°C + int active_heating_time = static_cast(256 * x[3] + x[2]); // -32768 to 32768s + double heating_overshoot_time = get_double(x[4], x[5], -64); // -512 to 512s + int active_schedule_day = static_cast(x[6]); + snprintf (buffer, sizeof(buffer), "Geyser Schedule: target %.2f°C, heating time %ds, overshoot %.1fs, day %d", active_schedule_temp, active_heating_time, heating_overshoot_time, active_schedule_day); + return buffer; + } + case CB_POWER_MAINS: + { + if(rtr) { + return "Request for power mains"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "Mains : %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_INVERTER: + { + if(rtr) { + return "Request for power inverter"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "InvOut: %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_PLUGS: + { + if(rtr) { + return "Request for power plugs"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "Plugs : %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_LIGHTS: + { + if(rtr) { + return "Request for power lights"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "Lights: %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_GEYSER: + { + if(rtr) { + return "Request for power geyser"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "Geyser: %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_POOL: + { + if(rtr) { + return "Request for power pool"; + } + double power = get_double(x[0], x[1], 2048); // 0 to 32kW + double voltage = get_double(x[2], x[3], 128); // 0 to 512V + double current = get_double(x[4], x[5], 512); // 0 to 128A + snprintf (buffer, sizeof(buffer), "Pool : %.3fV, %.3fA, %.3fkW", voltage, current, power); + return buffer; + } + case CB_POWER_GENERATED: + { + if(rtr) { + return "Request for power generated"; + } + double power_generated = get_double(x[0], x[1], 2048); // 0 to 32kW + double power_loss = get_double(x[2], x[3], 2048); // 0 to 32kW + snprintf (buffer, sizeof(buffer), "Power : Generated %.3fkW, Loss %.3fkW", power_generated, power_loss); + return buffer; + } + case CB_ENERGY_MAINS: + { + if(rtr) { + return "Request for energy mains"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Mains Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_GEYSER: + { + if(rtr) { + return "Request for energy geyser"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Geyser Energy day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_POOL: + { + if(rtr) { + return "Request for energy pool"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Pool Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_PLUGS: + { + if(rtr) { + return "Request for energy plugs"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Plugs Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_LIGHTS: + { + if(rtr) { + return "Request for energy lights"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Lights Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_HOUSE: + { + if(rtr) { + return "Request for energy house"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "House Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_GENERATED: + { + if(rtr) { + return "Request for energy generated"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Generated Energy: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_ENERGY_LOSS: + { + if(rtr) { + return "Request for energy loss"; + } + double energy_daily = get_double(x[0], x[1], 512); // 0 to 128kWh + double energy_monthly = get_double(x[2], x[3], 32); // 0 to 2048kWh + double energy_yearly = get_double(x[4], x[5], 2); // 0 to 32768kWh + double energy_lifetime = 0.01 * ((x[7] << 8) + x[6]); // 0 to 655MWh + snprintf (buffer, sizeof(buffer), "Energy Loss: day %.3fkWh, month %.3fkWh, year %.1fkWh, lifetime %.2fMWh", energy_daily, energy_monthly, energy_yearly, energy_lifetime); + return buffer; + } + case CB_CONTROLLER_STATES: + { + if(rtr) { + return "Request for controller states"; + } + uint8_t alarms = x[0]; + uint8_t states = x[1]; + uint8_t modes = x[2]; + bool inverter_1_battery_low = alarms & 0x80; + bool inverter_2_battery_low = alarms & 0x10; + bool inverter_overload = alarms & 0x08; + bool geyser_heating = states & 0x80; + bool geyser_energised = states & 0x40; + bool mains_supply_present = states & 0x20; + bool battery_charging = states & 0x08; + bool vacation_mode = modes & 0x80; // nobody at home + std::string mode = ""; + int geysermode = (modes & 0x70) >> 4; + switch (geysermode) { + case GM_SUNDAY: + mode = "SUN"; + break; + case GM_WORKDAY: + mode = "WRK"; + break; + case GM_SATURDAY: + mode = "SAT"; + break; + case GM_PUBLIC_HOLIDAY: + mode = "PUB"; + break; + case GM_SCHOOL_HOLIDAY: + mode = "SCH"; + break; + default: + mode = "UNK"; + break; + } + snprintf(buffer, sizeof(buffer), "STATES: %s%s%s%s MODE: %s%s INV: %s%s%s", geyser_heating ? "HEAT " : "", geyser_energised ? "GEYSER " : "", mains_supply_present ? "MAINS " : "", battery_charging ? "BATCHG " : "", vacation_mode ? "VACA ": "", mode.c_str(), inverter_1_battery_low ? "INV LOBAT " : "", inverter_2_battery_low ? "INV2 LOBAT " : "", inverter_overload ? "INV OVL " : ""); + return buffer; + } + + } + if(rtr) { + return "Request for unknown sthome CAN ID"; + } + return "Unknown CAN ID"; + } + +} // namespace solar \ No newline at end of file diff --git a/cbf_sthome.h b/cbf_sthome.h new file mode 100644 index 0000000..2016f87 --- /dev/null +++ b/cbf_sthome.h @@ -0,0 +1,78 @@ +#ifndef __SOLAR_CBF_STHOME_H // include GUARD +#define __SOLAR_CBF_STHOME_H +#include "esphome.h" +#include +#include "common.h" +#include "cb_frame.h" +using namespace esphome; + +namespace solar +{ + + class cbf_sthome : virtual public cb_frame { + private: + // float _get_battery_charge_voltage_limit() const; + + public: + // STHOME publish spec + // This is used to determine when to publish the sthome data + static const publish_spec_t publish_spec; // default publish spec for sthome messages + static const publish_spec_t rtr_publish_spec; // for remote transmission requests + + enum geyser_modes : int { + GM_SUNDAY, + GM_WORKDAY, + GM_SATURDAY, + GM_PUBLIC_HOLIDAY, + GM_SCHOOL_HOLIDAY, + }; + enum canbus_ids : int { + CB_CANBUS_ID01 = 0x501, + CB_CANBUS_ID02, + CB_CANBUS_ID03, + CB_CANBUS_ID04, + CB_CANBUS_ID05, + CB_CANBUS_ID06, + CB_CANBUS_ID07, + CB_CANBUS_ID08, + CB_CANBUS_ID09, + CB_CANBUS_ID10, + CB_POWER_MAINS = 0x401, + CB_POWER_INVERTER, + CB_POWER_PLUGS, + CB_POWER_LIGHTS, + CB_POWER_GEYSER, + CB_POWER_POOL, + CB_POWER_GENERATED, + CB_ENERGY_MAINS, + CB_ENERGY_GEYSER, + CB_ENERGY_POOL, + CB_ENERGY_PLUGS, + CB_ENERGY_LIGHTS, + CB_ENERGY_HOUSE, + CB_ENERGY_GENERATED, + CB_ENERGY_LOSS, + CB_CONTROLLER_STATES, + CB_GEYSER_TEMPERATURE_TOP, + CB_GEYSER_TEMPERATURE_BOTTOM, + CB_GEYSER_TEMPERATURE_AMBIENT, + CB_GEYSER_HEATING, + CB_GEYSER_ACTIVE_SCHEDULE, + }; + + //Property battery_charge_voltage_limit {this, &cbf_sthome::_set_battery_charge_voltage_limit, &cbf_sthome::_get_battery_charge_voltage_limit }; + //Property battery_charge_voltage_limit {this, &cbf_sthome::_get_battery_charge_voltage_limit}; + static const std::string heartbeat; + + cbf_sthome() = default; + cbf_sthome(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr); + void clear(); + virtual std::string to_string() const override; + cbf_sthome& operator=(cbf_sthome&& src); // required due to double inheritance in cbf_store_pylon + // using default (compiler auto generated) copy and move constructors and assignment operators + virtual ~cbf_sthome() = default; + bool verify_heartbeat() const; + }; + +} // namespace solar +#endif // __SOLAR_CBF_STHOME_H \ No newline at end of file diff --git a/cbf_store_sthome.cpp b/cbf_store_sthome.cpp new file mode 100644 index 0000000..b872d15 --- /dev/null +++ b/cbf_store_sthome.cpp @@ -0,0 +1,71 @@ +#include "cbf_store_sthome.h" + +namespace solar +{ + cbf_store_sthome::cbf_store_sthome(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr, time_t timestamp) + : cb_frame(msg_id, can_id, frame, rtr), cbf_store(msg_id, can_id, 0, timestamp, timestamp) //, cbf_sthome(msg_id, can_id, frame, rtr) + { + this->id = 0; + this->count = 0; + // ESP_LOGI(tag("store_sthome CTOR1").c_str(), "%-20s %s", "Created store_sthome", this->to_string().c_str()); + } + cbf_store_sthome::cbf_store_sthome(const cbf_store_sthome& b) + : cb_frame(b), cbf_store(b) // call base class copy constructor + { + // ESP_LOGI(tag("store_sthome CCTOR").c_str(), "%-20s %s", "Copied store_sthome", this->to_string().c_str()); + } + cbf_store_sthome& cbf_store_sthome::operator=(const cbf_store_sthome& b) + { + if (this != &b) { + cbf_store_sthome tmp(b); + swap(tmp); + // ESP_LOGI(tag("store_sthome AOPP").c_str(), "%-20s %s", "Assigned store_sthome", this->to_string().c_str()); + } + return *this; + } + cbf_store_sthome::cbf_store_sthome(cbf_store_sthome&& src) + : cb_frame(src), cbf_store(src) + { + src.clear(); + // ESP_LOGI(tag("store_sthome MCCTOR").c_str(), "%-20s %s", "Copied store_sthome", this->to_string().c_str()); + } + cbf_store_sthome& cbf_store_sthome::operator=(cbf_store_sthome&& src) + { + if (this != &src) { + cbf_store::operator=(std::move(src)); + cb_frame::operator=(std::move(src)); + src.clear(); + // ESP_LOGI(tag("store_sthome MOASS").c_str(), "%-20s %s", "Assigned store_sthome", this->to_string().c_str()); + } + return *this; + } + std::shared_ptr cbf_store_sthome::clone() const + { + return std::static_pointer_cast(clone_impl()); // static_pointer_cast is safe here because we know the underlying type is cbf_store_sthome + } + std::shared_ptr cbf_store_sthome::clone_impl() const + { + //ESP_LOGI(tag("store_sthome CLONE").c_str(), "%-20s", "Cloning store_sthome"); + return std::make_shared(*this); + } + void cbf_store_sthome::clear() + { + cb_frame::clear(); + cbf_store::clear(); + } + void cbf_store_sthome::swap(cbf_store_sthome &s) + { + cb_frame::swap(s); + cbf_store::swap(s); + } + cbf_store::cbf_updateresult cbf_store_sthome::update(const cbf_store& newitem, const int *comparecolumns) + { + //ESP_LOGI(tag("store_sthome UPDATE").c_str(), "%-20s %s", "Updating store_sthome", this->to_string().c_str()); + return cbf_store::update(newitem, publish_spec, rtr_publish_spec, comparecolumns); + } + std::string cbf_store_sthome::to_string() const + { + return cb_frame::to_string() + " " + cbf_sthome::to_string(); + } + +} // namespace solar \ No newline at end of file diff --git a/cbf_store_sthome.h b/cbf_store_sthome.h new file mode 100644 index 0000000..f8075f0 --- /dev/null +++ b/cbf_store_sthome.h @@ -0,0 +1,34 @@ +#ifndef __SOLAR_CBF_STORE_STHOME_H // include GUARD +#define __SOLAR_CBF_STORE_STHOME_H +#include "esphome.h" +#include +#include "common.h" +#include "cbf_store.h" +#include "cbf_sthome.h" +using namespace esphome; + +namespace solar +{ + class cbf_store_sthome : public cbf_store, public cbf_sthome + { + public: + cbf_store_sthome() = delete; // default constructor not allowed + cbf_store_sthome(int msg_id, uint32_t can_id, const byte_vector& _frame, bool _rtr , time_t timestamp); + virtual ~cbf_store_sthome() = default; // virtual destructor for base class + std::shared_ptr clone() const; + void clear(); + void swap(cbf_store_sthome &s); + virtual cbf_store::cbf_updateresult update(const cbf_store& newitem, const int *comparecolumns) override; + virtual std::string to_string() const override; + + cbf_store_sthome(const cbf_store_sthome& b); + cbf_store_sthome& operator=(const cbf_store_sthome& b); + cbf_store_sthome(cbf_store_sthome&& src); + cbf_store_sthome& operator=(cbf_store_sthome&& src); + + private: + virtual std::shared_ptr clone_impl() const override; + }; + +} // namespace solar +#endif // __SOLAR_CBF_STORE_STHOME_H \ No newline at end of file