From 56124beb983642791f46d3224d4ad29c40669bd2 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 18 Jan 2026 23:57:20 +0200 Subject: [PATCH] Added ds3231 external component. Restored sthome-ut8.yaml --- components/ds3231/__init__.py | 0 components/ds3231/ds3231.cpp | 111 + components/ds3231/ds3231.h | 70 + components/ds3231/time.py | 58 + components/tlc59208f_ext/tlc59208f_output.cpp | 4 +- sthome-ut8.yaml | 4388 +++++++++++++++-- 6 files changed, 4330 insertions(+), 301 deletions(-) create mode 100644 components/ds3231/__init__.py create mode 100644 components/ds3231/ds3231.cpp create mode 100644 components/ds3231/ds3231.h create mode 100644 components/ds3231/time.py diff --git a/components/ds3231/__init__.py b/components/ds3231/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/ds3231/ds3231.cpp b/components/ds3231/ds3231.cpp new file mode 100644 index 0000000..7f0da7c --- /dev/null +++ b/components/ds3231/ds3231.cpp @@ -0,0 +1,111 @@ +#include "ds3231.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://datasheets.maximintegrated.com/en/ds/DS3231.pdf + +namespace esphome { +namespace ds3231 { + +static const char *const TAG = "ds3231"; + +void DS3231Component::setup() { + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void DS3231Component::update() { this->read_time(); } + +void DS3231Component::dump_config() { + ESP_LOGCONFIG(TAG, "DS3231:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + if (!this->read_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + } +} + +float DS3231Component::get_setup_priority() const { return setup_priority::DATA; } + +void DS3231Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (ds3231_.reg.ch) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + ESPTime rtc_time{ + .second = uint8_t(ds3231_.reg.second + 10 * ds3231_.reg.second_10), + .minute = uint8_t(ds3231_.reg.minute + 10u * ds3231_.reg.minute_10), + .hour = uint8_t(ds3231_.reg.hour + 10u * ds3231_.reg.hour_10), + .day_of_week = uint8_t(ds3231_.reg.weekday), + .day_of_month = uint8_t(ds3231_.reg.day + 10u * ds3231_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds3231_.reg.month + 10u * ds3231_.reg.month_10), + .year = uint16_t(ds3231_.reg.year + 10u * ds3231_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void DS3231Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + ds3231_.reg.year = (now.year - 2000) % 10; + ds3231_.reg.year_10 = (now.year - 2000) / 10 % 10; + ds3231_.reg.month = now.month % 10; + ds3231_.reg.month_10 = now.month / 10; + ds3231_.reg.day = now.day_of_month % 10; + ds3231_.reg.day_10 = now.day_of_month / 10; + ds3231_.reg.weekday = now.day_of_week; + ds3231_.reg.hour = now.hour % 10; + ds3231_.reg.hour_10 = now.hour / 10; + ds3231_.reg.minute = now.minute % 10; + ds3231_.reg.minute_10 = now.minute / 10; + ds3231_.reg.second = now.second % 10; + ds3231_.reg.second_10 = now.second / 10; + ds3231_.reg.ch = false; + + this->write_rtc_(); +} + +bool DS3231Component::read_rtc_() { + if (!this->read_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds3231_.reg.hour_10, + ds3231_.reg.hour, ds3231_.reg.minute_10, ds3231_.reg.minute, ds3231_.reg.second_10, ds3231_.reg.second, + ds3231_.reg.year_10, ds3231_.reg.year, ds3231_.reg.month_10, ds3231_.reg.month, ds3231_.reg.day_10, + ds3231_.reg.day, ONOFF(ds3231_.reg.ch), ds3231_.reg.rs, ONOFF(ds3231_.reg.sqwe), ONOFF(ds3231_.reg.out)); + + return true; +} + +bool DS3231Component::write_rtc_() { + if (!this->write_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds3231_.reg.hour_10, + ds3231_.reg.hour, ds3231_.reg.minute_10, ds3231_.reg.minute, ds3231_.reg.second_10, ds3231_.reg.second, + ds3231_.reg.year_10, ds3231_.reg.year, ds3231_.reg.month_10, ds3231_.reg.month, ds3231_.reg.day_10, + ds3231_.reg.day, ONOFF(ds3231_.reg.ch), ds3231_.reg.rs, ONOFF(ds3231_.reg.sqwe), ONOFF(ds3231_.reg.out)); + return true; +} +} // namespace ds3231 +} // namespace esphome diff --git a/components/ds3231/ds3231.h b/components/ds3231/ds3231.h new file mode 100644 index 0000000..da8352a --- /dev/null +++ b/components/ds3231/ds3231.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace ds3231 { + +class DS3231Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union DS3231Reg { + struct { + uint8_t second : 4; + uint8_t second_10 : 3; + bool ch : 1; + + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t unused_1 : 1; + + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t unused_2 : 2; + + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t unused_4 : 2; + + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t unused_5 : 3; + + uint8_t year : 4; + uint8_t year_10 : 4; + + uint8_t rs : 2; + uint8_t unused_6 : 2; + bool sqwe : 1; + uint8_t unused_7 : 2; + bool out : 1; + } reg; + mutable uint8_t raw[sizeof(reg)]; + } ds3231_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace ds3231 +} // namespace esphome diff --git a/components/ds3231/time.py b/components/ds3231/time.py new file mode 100644 index 0000000..0f57c39 --- /dev/null +++ b/components/ds3231/time.py @@ -0,0 +1,58 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_ID + +# adapted from DS1307 code by badbadc0ffee +CODEOWNERS = ["@stuurmcp"] +DEPENDENCIES = ["i2c"] +ds3231_ns = cg.esphome_ns.namespace("ds3231") +DS3231Component = ds3231_ns.class_("DS3231Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = ds3231_ns.class_("WriteAction", automation.Action) +ReadAction = ds3231_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DS3231Component), + } +).extend(i2c.i2c_device_schema(0x68)) + + +@automation.register_action( + "ds3231.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DS3231Component), + } + ), +) +async def ds3231_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "ds3231.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DS3231Component), + } + ), +) +async def ds3231_read_time_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 + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/components/tlc59208f_ext/tlc59208f_output.cpp b/components/tlc59208f_ext/tlc59208f_output.cpp index 6ad6493..638c7fb 100644 --- a/components/tlc59208f_ext/tlc59208f_output.cpp +++ b/components/tlc59208f_ext/tlc59208f_output.cpp @@ -182,7 +182,9 @@ void TLC59208FOutput::loop() uint8_t reg = TLC59208F_REG_PWM0 + channel; if (!this->write_byte(reg, pwm)) { - this->status_set_warning(); + char buffer[128]; + snprintf(buffer, sizeof(buffer), "Writing pwm value %d to channel %d failed.", pwm, channel); + this->status_set_warning(buffer); return; } } diff --git a/sthome-ut8.yaml b/sthome-ut8.yaml index d0caf83..fe8aa9b 100644 --- a/sthome-ut8.yaml +++ b/sthome-ut8.yaml @@ -1,30 +1,208 @@ -# SIMPLIFIED CONFIGURATION FILE FOR sthome-ut8 DEVICE +###### sthome-ut8 ###### + external_components: - source: type: local path: components # Path relative to this YAML file - components: [ ads1115_int, ads1115_pol, tlc59208f_ext ] #, ads131m08 ] + components: [ ads1115_int, tlc59208f_ext, ds3231 ] #, ads131m08 ] packages: - !include common/wifi.yaml - + - !include common/canbus.yaml + - !include common/geyser.yaml + - !include common/felicityinverter.yaml +# - device: !include device.yaml substitutions: name: sthome-ut8 friendly_name: "sthome-ut8" - + MAX_SCHOOL_HOLIDAY_PERIODS: 12 + BATTERY_INFO_TIMEOUT: 30 # 30 sec timeout esphome: name: "${name}" friendly_name: "${friendly_name}" - +# platformio_options: +# build_flags: -fexceptions +# build_unflags: -fno-exceptions + includes: + - source # copies folder with files to relevant to be included in esphome compile + - # angle brackets ensure file is included above globals in main.cpp. Make sure to use include GUARDS in the file to prevent double inclusion + - + - + - + - + - + - on_boot: - then: - - lambda: |- - ESP_LOGI("esphome", "Device booted"); - delay(300); - //id(tlc59208f_1)->test(); - //id(tlc59208f_1)->serial_test(); + - priority: 600 # This is where most sensors are set up (higher number means higher priority) + then: + - ds3231.read_time: # read the RTC time + - lambda: |- + id(geyser_relay).turn_off(); + id(pool_relay).turn_off(); + id(timer_start) = 0; + id(can1_msgctr) = 0; + id(can2_msgctr) = 0; + id(g_cb_request_queue) = std::queue< std::set >(); + id(time_synched) = false; + id(init_fixed_public_holidays).execute(); + id(init_schedule).execute(); + - uart.write: + id: inv_uart1 + data: [0x0D, 0x0A] + - uart.write: + id: inv_uart2 + data: [0x0D, 0x0A] + - delay: 300ms + +globals: + - id: gb_geyser_disable + type: bool + restore_value: true + initial_value: 'false' + - id: gb_temp_top + type: double + - id: gb_temp_bottom + type: double + - id: g_inv1_power_flow + type: uint16_t + restore_value: no + initial_value: '0' + - id: g_inv2_power_flow + type: uint16_t + restore_value: no + initial_value: '0' + - id: time_synched + type: bool + restore_value: no + initial_value: 'false' + - id: geyser_relay_status + type: bool + restore_value: yes + initial_value: 'false' + - id: sun_elevation_minimum + type: double + restore_value: yes + initial_value: '30.0' + - id: thermal_transmittance + type: double + restore_value: yes + initial_value: '1.876' # '1.31' # geyser insulation thermal transmittance in W/m²K + - id: geyser_surface_area + type: double + restore_value: yes + initial_value: '3.088' # in square metres / radius = 0.26m, length = 1.63m + - id: geyser_element_resistance + type: double + restore_value: yes + initial_value: '17.69' # in ohms - 230v / 13A = 17.69 ohm - amounts to 2.99kW at 230v + - id: watermass + type: double + restore_value: yes + initial_value: '300' # 300 litres = 300 kg + - id: geyser_top_bottom_constraint + type: double + restore_value: yes + initial_value: '17' # in °C, difference between top and bottom temperature at which it will start influencing the heat_required calculation + - id: temp_overshoot_allowed + type: double + restore_value: yes + initial_value: '0.25' # switch off geyser if temperature is this value or more above target temperature. This value also acts as top end hysteresis + - id: active_schedule_temperature + type: double + restore_value: yes + initial_value: '50.0' + - id: active_heating_time + type: int + initial_value: '0' + restore_value: yes + - id: estimated_heating_time + type: int + restore_value: no + - id: estimated_heating_overshoot_time + type: int + restore_value: no + - id: g_heat_loss + type: double + initial_value: '0' + restore_value: no + - id: geyser_effective_power + type: double + restore_value: no + - id: heat_monitor_start + type: time_t + initial_value: '0' + restore_value: yes + - id: heat_monitor_end + type: time_t + initial_value: '0' + restore_value: yes + - id: g_heat_gained + type: double + initial_value: '0' + restore_value: yes + - id: last_geyser_top_temperature + type: double + initial_value: '-301' # less than -300 denotes that temperature was not updated yet + restore_value: yes + - id: last_temp_diff + type: double + initial_value: '0' + restore_value: yes + - id: timer_start + type: time_t + initial_value: '0' + restore_value: yes + - id: geyser_day_ind + type: int + initial_value: '0' + restore_value: yes + - id: active_heating_start + type: time_t + initial_value: '1749538816' # 2025-06-10 09:00 + restore_value: no + - id: active_heating_end + type: time_t + initial_value: '1749538816' # 2025-06-10 09:00 + restore_value: no + - id: active_schedule_period + type: int[2] + initial_value: '{0, 0}' + restore_value: no + - id: g_schedule + type: int[${GEYSER_MODES}][${HEATING_DAY_BLOCKS}][3] + restore_value: no # initialised by script + - id: fixed_public_holidays + type: int[10][2] + restore_value: no # initialised by script + - id: public_holidays + type: int[12][2] + restore_value: no + - id: school_holidays + type: int[${MAX_SCHOOL_HOLIDAY_PERIODS}][2][2] # SCHOOL holiday periods - format for a period: {{start month, start day of month}, {end month, end day of month}} + restore_value: yes + initial_value: '{{{{1, 1}, {1, 14}}, {{3, 28}, {4, 17}}, {{4, 29}, {4, 30}}, {{5, 2}, {5, 2}}, {{6, 28}, {7, 21}}, {{10, 4}, {10, 12}}, {{12, 10}, {12, 31}}, {{0, 0}, {0, 0}}}}' + - id: energy_counters_reset_time + type: time_t + initial_value: '0' + restore_value: yes + + - id: can1_msgctr + type: int + restore_value: no + - id: can2_msgctr + type: int + restore_value: no + - id: last_battery_message_time + type: time_t + restore_value: no + - id: g_cb_cache # the cache is used to only accept a frame after it has been received a specified number of times within a specified period. this hopefully will iron out spurious corrupted frames + type: solar::cbf_cache + restore_value: no + - id: g_cb_request_queue + type: std::queue< std::set > + restore_value: no # esp32: board: esp32dev @@ -38,7 +216,7 @@ debug: # Enable logging logger: - level: VERY_VERBOSE + level: VERBOSE initial_level: INFO logs: canbus: INFO @@ -47,15 +225,12 @@ logger: uart: INFO light: INFO sensor: INFO - ds1307: INFO + ds3231: DEBUG tlc59208f_ext: VERBOSE text_sensor: INFO - ads1115.sensor: INFO - ads1115_pol: DEBUG - ads1115_pol.sensor: INFO - ads1115_int: DEBUG - ads1115_int.sensor: INFO - modbus: INFO + ads1115_int: VERBOSE + ads1115_int.sensor: VERBOSE + modbus: VERBOSE modbus_controller: INFO modbus_controller.sensor: INFO @@ -80,54 +255,11 @@ wifi: captive_portal: -spi: - - id: spi_bus0 - clk_pin: GPIO18 - mosi_pin: GPIO23 - miso_pin: GPIO19 - interface: any -time: -# - platform: ds1307 -# update_interval: never - - - platform: homeassistant - id: time_source -# on_time_sync: -# - ds1307.write_time: -# - lambda: |- -# id(time_synched) = true; -# id(init_holidays).execute(); // we need valid time to calculate holidays -# // id(show_schedule).execute(); // for debugging - - # - logger.log: "Synchronized system clock" -# -tlc59208f_ext: - address: 0x20 - id: tlc59208f_1 - i2c_id: bus_b - reset_pin: - number: GPIO25 - mode: - output: true - pullup: true - -#interval: -# - interval: 5s -# then: -# - lambda: |- -# id(tlc59208f_1)->test(); - -binary_sensor: - - platform: status - # Status platform provides a connectivity sensor - name: "Status" - device_class: connectivity - -switch: - - platform: restart - name: "${name} Restart" - id: "restart_switch" +one_wire: + - platform: gpio + pin: GPIO4 + id: geyser_temperature_sensors i2c: - id: bus_a @@ -136,8 +268,8 @@ i2c: scan: true frequency: 400kHz - id: bus_b - sda: GPIO16 - scl: GPIO17 + sda: GPIO15 + scl: GPIO26 scan: true frequency: 20kHz sda_pullup_enabled: true @@ -177,225 +309,693 @@ ads1115_int: input: true pullup: false # external 10k pullup on ads1115 dev board -sensor: - - platform: debug - loop_time: - name: "Loop Time" +spi: + - id: spi_bus0 + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + interface: any - # NB! Keep all ads1115 sample rates the same. Update intervals should be more than or equal to 1/sample_rate - # ads1115_48 - - platform: ads1115_int - multiplexer: 'A0_A1' - gain: 2.048 # 4.096 - ads1115_id: ads1115_48 - sample_rate: 860 # 475 #860 - state_class: measurement - device_class: current - accuracy_decimals: 8 - name: "ADC House Current" - id: power_outlets_current - unit_of_measurement: "A" - icon: "mdi:current" +uart: + - id: inv_uart1 + tx_pin: GPIO32 # Tx1 + rx_pin: + number: GPIO34 # Rx1 + inverted: false + mode: + input: true + pullup: false # external pullup + baud_rate: 2400 + stop_bits: 1 + parity: NONE +# debug: +# direction: BOTH +# dummy_receiver: false +# after: +# delimiter: "\r" +# sequence: +# - lambda: UARTDebug::log_hex(direction, bytes, ','); - - platform: ads1115_int - multiplexer: 'A2_A3' - gain: 2.048 # 4.096 - ads1115_id: ads1115_48 - name: "ADC Geyser Current" - id: geyser_current - sample_rate: 860 #860 - state_class: measurement - device_class: current - accuracy_decimals: 8 - unit_of_measurement: "A" - icon: "mdi:current" - filters: - - lambda: return x * x; - - sliding_window_moving_average: - window_size: 625 #1250 #5000 - send_every: 104 #208 #416 - send_first_at: 104 #208 #416 - - lambda: return sqrt(x); - - multiply: 100 #92.1 #91.1 #88.44 - - offset: 0.0 #-0.2 - # - lambda: |- - # if(abs(x) < 0.1) - # return 0.0; - # return x; - on_value: - then: - - lambda: |- - id(set_indicators).execute(0.0, x); - on_value_range: - - below: 5.0 - then: - - lambda: |- - ESP_LOGI("geyser", "No geyser current detected. Geyser not heating."); - - above: 5.0 + - id: inv_uart2 + tx_pin: GPIO33 # Tx2 + rx_pin: + number: GPIO35 # Rx2 + inverted: false + mode: + input: true + pullup: false # external pullup + baud_rate: 2400 + stop_bits: 1 + parity: NONE +# debug: +# direction: BOTH +# dummy_receiver: false +# after: +# delimiter: "\r" +# sequence: +# - lambda: UARTDebug::log_hex(direction, bytes, ' '); + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: + - platform: ds3231 + address: 0x68 + i2c_id: bus_a +# # repeated synchronization is not necessary unless the external RTC +# # is much more accurate than the internal clock + update_interval: never +# +# - platform: sntp +# timezone: Africa/Johannesburg +# servers: +# - ntp1.meraka.csir.co.za # 146.64.24.58 +# - ntp.as3741.net # 196.4.160.4 +# - ntp1.inx.net.za # 196.10.52.57 + - platform: homeassistant + id: time_source + on_time_sync: + - ds3231.write_time: + - lambda: |- + id(time_synched) = true; + id(init_holidays).execute(); // we need valid time to calculate holidays +# // id(show_schedule).execute(); // for debugging + + - logger.log: "Synchronized system clock" + on_time: +# # do every year on the first day of the first month at one second after midnight +# - seconds: 1 +# minutes: 0 +# hours: 0 +# days_of_month: 1 +# months: 1 +# then: +# - sensor.integration.reset: yearly_geyser_energy +# - sensor.integration.reset: yearly_plugs_energy +# - sensor.integration.reset: yearly_mains_energy +# - sensor.integration.reset: yearly_lights_energy +# - sensor.integration.reset: yearly_generated_energy +# - sensor.integration.reset: yearly_house_energy_usage +# - sensor.integration.reset: yearly_energy_loss + # do every first day of month at one second after midnight + - seconds: 1 + minutes: 0 + hours: 0 + days_of_month: 1 + then: + - sensor.integration.reset: monthly_geyser_energy + - sensor.integration.reset: monthly_plugs_energy + - sensor.integration.reset: monthly_mains_energy + - sensor.integration.reset: monthly_lights_energy + - sensor.integration.reset: monthly_generated_energy + - sensor.integration.reset: monthly_house_energy_usage + - sensor.integration.reset: monthly_energy_loss + +# # do every day at one second after midnight +# - seconds: 1 +# minutes: 0 +# hours: 0 +# then: +# - lambda: |- +# id(init_daily_power_counters).execute(); + + # do every 15 minutes + - seconds: 0 + minutes: 10, 25, 40, 55 then: - lambda: |- - ESP_LOGI("geyser", "Geyser current detected. Geyser was energised."); + id(record_heat_gained).execute(); - # ads1115_49 - - platform: ads1115_int - multiplexer: 'A2_A3' - gain: 2.048 # 4.096 - ads1115_id: ads1115_49 - name: "ADC Mains Current" - id: mains_current - sample_rate: 860 #475 - state_class: measurement - device_class: current - accuracy_decimals: 8 - unit_of_measurement: "A" - icon: "mdi:current" +# # do every second + - seconds: '*' + minutes: '*' + then: + - lambda: |- + const char tag[] = "ADC\0"; + // id(get_ha_settings).execute(); + //id(update_power_counters).execute(); + //ESP_LOGI(tag, "Mains: %f", id(mains_voltage_adc).state); + //ESP_LOGI(tag, "Inverter Out: %f", id(inverter_output_voltage_adc).state); + //ESP_LOGI(tag, "Inverter Out: %f, Mains: %f, Spare1: %f, Spare2: %f,", id(inverter_output_voltage_adc).state, id(mains_voltage_adc).state, id(spare1_voltage_adc).state, id(spare2_voltage_adc).state); + //ESP_LOGI(tag, "AMP: Ge %.4f, Li: %.4f, Ma %.4f, Pl:%.4f, VOLT: Ma: %.4f, Pl %.4f, A2: %.4f, A3 %.4f, TEMP: %.4f", id(geyser_current).state, id(lights_current).state, id(mains_current).state, id(power_outlets_current).state, id(mains_voltage_adc).state, id(inverter_output_voltage_adc).state, id(adc4A_A2).state, id(adc4A_A3).state, top_temp); + //ESP_LOGI(tag, "AMP: Ge %.4f, Li: %.4f, Ma %.4f, Pl:%.4f, VOLT: Ma: %.8f, Pl %.8f, TEMP: %.4f", id(geyser_current).state, id(lights_current).state, id(mains_current).state, id(power_outlets_current).state, id(mains_voltage_adc).state, id(inverter_output_voltage_adc).state, top_temp); + + - text_sensor.template.publish: + id: heating_time_text + state: !lambda |- + int seconds = id(active_heating_time); + int days = seconds / (24 * 3600); + seconds = seconds % (24 * 3600); + int hours = seconds / 3600; + seconds = seconds % 3600; + int minutes = seconds / 60; + seconds = seconds % 60; + auto days_str = std::to_string(days); + auto hours_str = std::to_string(hours); + auto minutes_str = std::to_string(minutes); + auto seconds_str = std::to_string(seconds); + return ( + (days ? days_str + "d " : "") + + (hours ? hours_str + "h " : "") + + (minutes ? minutes_str + "m " : "") + + (seconds_str + "s") + ).c_str(); + + - text_sensor.template.publish: + id: heating_start_text + state: !lambda |- + auto time_obj = ESPTime::from_epoch_local(id(active_heating_start)); + return time_obj.strftime("%Y-%m-%d %H:%M:%S"); - - platform: ads1115_int - multiplexer: A0_A1 - gain: 2.048 # 4.096 - ads1115_id: ads1115_49 - name: "ADC Lights Current" - id: lights_current - sample_rate: 860 - # update_interval: 10ms - # id: lights_current_adc - state_class: measurement - device_class: current - accuracy_decimals: 8 - # mod ########################### - unit_of_measurement: "A" - icon: "mdi:current" - filters: - - lambda: return x * x; - - sliding_window_moving_average: - window_size: 625 #1250 #5000 - send_every: 104 #208 #416 - send_first_at: 104 #208 #416 - - lambda: return sqrt(x); - - multiply: 100 #92.1 #91.1 #88.44 - - offset: 0.0 #-0.2 - # - lambda: |- - # if(abs(x) < 0.1) - # return 0.0; - # return x; + - text_sensor.template.publish: + id: heating_end_text + state: !lambda |- + auto time_obj = ESPTime::from_epoch_local(id(active_heating_end)); + return time_obj.strftime("%Y-%m-%d %H:%M:%S"); + + - text_sensor.template.publish: + id: active_schedule_start_text + state: !lambda |- + auto time_obj = ESPTime::from_epoch_local(id(active_schedule_period)[0]); + return time_obj.strftime("%Y-%m-%d %H:%M:%S"); - # ads1115_4A - # Inverter voltage sensor - - platform: ads1115_int - ads1115_id: ads1115_4A - sample_rate: 860 - name: "ADC Mains Voltage" - id: mains_voltage_adc - unit_of_measurement: "V" - accuracy_decimals: 8 - icon: "mdi:flash" - multiplexer: A0_GND - gain: 2.048 # 4.096 - #update_interval: 8ms #5ms #23ms - device_class: voltage - state_class: measurement -# filters: -# - offset: -2.048 #-2.04794027 # 0.0131 -# - lambda: return x * x; -# - sliding_window_moving_average: -# window_size: 1250 #1250 -# send_every: 208 -# send_first_at: 208 -# - lambda: return sqrt(x); -# - multiply: 766.6670 # 930 #650 - #- lambda: |- - # if(abs(x) < 10) - # return 0; - # return x;# + - text_sensor.template.publish: + id: active_schedule_end_text + state: !lambda |- + auto time_obj = ESPTime::from_epoch_local(id(active_schedule_period)[1]); + return time_obj.strftime("%Y-%m-%d %H:%M:%S"); +# +interval: + - interval: 30s + then: + - script.execute: set_geyser_relay - # ads1115_4A - # Mains voltage sensor - - platform: ads1115_int - ads1115_id: ads1115_4A - sample_rate: 860 - name: "ADC House Voltage" - id: inverter_output_voltage_adc - unit_of_measurement: "V" - accuracy_decimals: 8 - icon: "mdi:flash" - multiplexer: A2_GND - gain: 2.048 # 4.096 - #update_interval: 8ms #5ms #23ms - device_class: voltage - state_class: measurement -# filters: -# - offset: -2.048 #-2.0491 #4.096 #0.0065 -# - lambda: return x * x; -# - sliding_window_moving_average: -# window_size: 1250 #625 #1250 -# send_every: 208 #104 -# send_first_at: 208 #104 #416 -# - lambda: return sqrt(x); -# - multiply: 766.6670 # 930 #650 - #- lambda: |- - # if(abs(x) < 20) - # return 0; - # return x; + - interval: 1s + then: + - script.execute: set_active_schedule + - script.execute: set_active_heating_timers + - script.execute: reset_geyser_relay + - lambda: |- + id(set_heat_indicators).execute(id(geyser_bottom_temperature).state, id(geyser_top_temperature).state); + // for debugging + // id(set_heat_indicators).execute(id(geyser_bottom_temp).state, id(geyser_top_temp).state); - - platform: ads1115_int - ads1115_id: ads1115_4A - sample_rate: 860 - name: "ADC Spare1 Voltage" - unit_of_measurement: "V" - accuracy_decimals: 8 - icon: "mdi:flash" - multiplexer: A1_GND - gain: 2.048 # 4.096 - #update_interval: 8ms #5ms #23ms - device_class: voltage - state_class: measurement + - interval: 5s + then: + - script.execute: send_battery_info_request - - platform: ads1115_int - ads1115_id: ads1115_4A - sample_rate: 860 - name: "ADC Spare2 Voltage" - unit_of_measurement: "V" - accuracy_decimals: 8 - icon: "mdi:flash" - multiplexer: A3_GND - gain: 2.048 # 4.096 - #update_interval: 8ms #5ms #23ms - device_class: voltage - state_class: measurement + - interval: 100ms + then: + lambda: |- + using namespace solar; + bool success = false; + if(!id(g_cb_request_queue).empty()) { + auto canid_set = id(g_cb_request_queue).front(); + std::set unhandled_set, failed_set; + for(auto& can_id : canid_set) { + switch(can_id) { + // pylon ids + case cbf_pylon::CB_BATTERY_LIMITS: + id(canbus_send_battery_limits).execute(); + break; + case cbf_pylon::CB_BATTERY_STATE: + id(canbus_send_battery_state).execute(); + break; + case cbf_pylon::CB_BATTERY_STATUS: + id(canbus_send_battery_status).execute(); + break; + case cbf_pylon::CB_BATTERY_FAULT: + id(canbus_send_battery_fault).execute(); + break; + case cbf_pylon::CB_BATTERY_REQUEST_FLAGS: + id(canbus_send_battery_request_flags).execute(); + break; + case cbf_pylon::CB_BATTERY_MANUFACTURER: + id(canbus_send_battery_manufacturer).execute(); + break; + // sthome ids + case cbf_sthome::CB_POWER_MAINS: + id(canbus_send_power_mains).execute(); + break; + case cbf_sthome::CB_POWER_INVERTER: + id(canbus_send_power_inverter).execute(); + break; + case cbf_sthome::CB_POWER_PLUGS: + id(canbus_send_power_plugs).execute(); + break; + case cbf_sthome::CB_POWER_LIGHTS: + id(canbus_send_power_lights).execute(); + break; + case cbf_sthome::CB_POWER_GEYSER: + id(canbus_send_power_geyser).execute(); + break; + //case cbf_sthome::CB_POWER_POOL: + // id(canbus_send_power_pool).execute(); + // break; + case cbf_sthome::CB_POWER_GENERATED: + id(canbus_send_power_generated).execute(); + break; + case cbf_sthome::CB_ENERGY_MAINS: + id(canbus_send_energy_mains).execute(); + break; + case cbf_sthome::CB_ENERGY_GEYSER: + id(canbus_send_energy_geyser).execute(); + break; + case cbf_sthome::CB_ENERGY_POOL: + //id(canbus_send_energy_pool).execute(); + break; + case cbf_sthome::CB_ENERGY_PLUGS: + id(canbus_send_energy_plugs).execute(); + break; + case cbf_sthome::CB_ENERGY_LIGHTS: + id(canbus_send_energy_lights).execute(); + break; + case cbf_sthome::CB_ENERGY_HOUSE: + id(canbus_send_energy_house).execute(); + break; + case cbf_sthome::CB_ENERGY_GENERATED: + id(canbus_send_energy_generated).execute(); + break; + case cbf_sthome::CB_ENERGY_LOSS: + id(canbus_send_energy_loss).execute(); + break; + case cbf_sthome::CB_GEYSER_TEMPERATURE_TOP: + id(canbus_send_temperature_top).execute(success); + if(!success) { + failed_set.insert(can_id); + } + break; + case cbf_sthome::CB_GEYSER_TEMPERATURE_BOTTOM: + id(canbus_send_temperature_bottom).execute(success); + if(!success) { + failed_set.insert(can_id); + } + break; + case cbf_sthome::CB_GEYSER_TEMPERATURE_AMBIENT: + id(canbus_send_temperature_ambient).execute(success); + if(!success) { + failed_set.insert(can_id); + } + break; + case cbf_sthome::CB_CANBUS_ID08: + id(canbus_send_heartbeat).execute(); + break; + default: + unhandled_set.insert(can_id); + } + } + id(g_cb_request_queue).pop(); // remove from queue + // do remaining can_ids, if any + bool time_isvalid = id(time_source).now().is_valid(); + if(time_isvalid) { + for(auto& can_id : unhandled_set) { + switch(can_id) { + case cbf_sthome::CB_CONTROLLER_STATES: + // id(canbus_send_controller_states).execute(); + break; + case cbf_sthome::CB_GEYSER_HEATING: + id(canbus_send_geyser_heating).execute(); + break; + case cbf_sthome::CB_GEYSER_ACTIVE_SCHEDULE: + id(canbus_send_geyser_active_schedule).execute(); + break; + default: + ESP_LOGW("Unknown CAN_ID", "CAN_ID: 0x%X. Remote transmission request ignored!", can_id); + break; + } + } + } + else { + // re-insert unhandled can-ids to the back of the queue + id(canbus_add_to_queue).execute(unhandled_set, 5); + } + id(canbus_add_to_queue).execute(failed_set, 5); + } + + - interval: ${CB_RETRANSMISSION_INTERVAL} + then: + lambda: |- + using namespace solar; + // we use the cache to handle recently received remote transmission requests. this allows for ironing out invalid (non-repeated) frames + ESP_LOGV("processing RTRs ", "%d publishable request(s) in cache", std::count_if(id(g_cb_cache).cache_map.begin(), id(g_cb_cache).cache_map.end(), [](auto& it) { auto& store = it.second.get_store(); return store.rtr && store.getpublish(); })); + // we have an outer loop to queue 5 blocks of the requested frames to ensure delivery + for(int i = 0; i < ${CB_MAX_RETRANSMISSIONS}; i++) { + std::set canid_set; + for(auto& kvp : id(g_cb_cache).cache_map) { + const auto& item = kvp.second; + auto& store = item.get_store(); + if(store.rtr) { + //ESP_LOGI(store.tag().c_str(), "%s", store.to_string().c_str()); + if(store.getpublish()) { + canid_set.insert(store.can_id); + if(i == ${CB_MAX_RETRANSMISSIONS} - 1) { + ESP_LOGV(store.tag().c_str(), "%s", store.to_string().c_str()); // we display once, using opportunity at end of sequence (when publish flag is reset) + store.setpublish(false); + } + } + } + } + id(g_cb_request_queue).push(canid_set); + } - # Report wifi signal strength every 5 min if changed - - platform: wifi_signal - name: WiFi Signal - update_interval: 300s - filters: - - delta: 10% - # human readable uptime sensor output to the text sensor above - - platform: uptime - name: Uptime in Days - id: uptime_sensor_days - update_interval: 10s - on_raw_value: +modbus: + - id: modbus1 + uart_id: inv_uart1 + send_wait_time: 1200ms #250ms + disable_crc: false + role: client + + - id: modbus2 + uart_id: inv_uart2 + send_wait_time: 1200ms #250ms + disable_crc: false + role: client + +modbus_controller: + - id: modbus_device1 + modbus_id: modbus1 + address: 0x01 + allow_duplicate_commands: False + command_throttle: 700ms #2022ms + update_interval: 30s #305s + offline_skip_updates: 2 + max_cmd_retries: 1 + setup_priority: -10 + - id: modbus_device2 + modbus_id: modbus2 + address: 0x01 + allow_duplicate_commands: False + command_throttle: 0ms + update_interval: 30s #30s + offline_skip_updates: 2 + max_cmd_retries: 1 + setup_priority: -10 + +canbus: + - platform: mcp2515 + cs_pin: GPIO13 # CB1CS + spi_id: spi_bus0 + id: canbus_sthome + mode: NORMAL + can_id: ${CB_CANBUS_ID08} + bit_rate: 500KBPS + on_frame: + - can_id: 0 + can_id_mask: 0 then: - - text_sensor.template.publish: - id: uptime_human - state: !lambda |- - int seconds = round(id(uptime_sensor_days).raw_state); - int days = seconds / (24 * 3600); - seconds = seconds % (24 * 3600); - int hours = seconds / 3600; - seconds = seconds % 3600; - int minutes = seconds / 60; - seconds = seconds % 60; - auto days_str = std::to_string(days); - auto hours_str = std::to_string(hours); - auto minutes_str = std::to_string(minutes); - auto seconds_str = std::to_string(seconds); - return ( - (days ? days_str + "d " : "") + - (hours ? hours_str + "h " : "") + - (minutes ? minutes_str + "m " : "") + - (seconds_str + "s") - ).c_str(); + - lambda: |- + id(can2_msgctr)++; + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + if(can_id >= 0x350 && can_id < 0x380) { + auto cbitem = cbf_store_pylon(id(can2_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + //ESP_LOGI(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + ESP_LOGV(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + } + } + else if(can_id >= 0x400 && can_id <= 0x580) { + auto cbitem = cbf_store_sthome(id(can2_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + //ESP_LOGI(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + ESP_LOGV(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + } + } + else { + ESP_LOGW("canbus", "Request within unhandled range CAN_ID: 0x%X. Request ignored!", can_id); + } + } + + - platform: mcp2515 + cs_pin: GPIO14 # CB2CS + spi_id: spi_bus0 + id: canbus_solarbattery + mode: NORMAL + can_id: ${CB_CANBUS_ID08} + bit_rate: 500KBPS + on_frame: + - can_id: 0 + can_id_mask: 0 + then: + - lambda: |- + id(can1_msgctr)++; + auto time_obj = id(time_source).now(); + id(last_battery_message_time) = time_obj.timestamp; + //id(canbus_sthome)->send_data(can_id, false, x); + //ESP_LOGI("SND_BAT", "0x%X", can_id); + + - can_id: ${CB_BATTERY_LIMITS} # 0x351 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + //ESP_LOGI(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + float value = 0.1 * ((x[1] << 8) + x[0]); // unit = 0.1V + id(battery_charge_voltage_limit).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A + id(battery_charge_current_limit).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1A + id(battery_discharge_current_limit).publish_state(value); + cbitem.setpublish(false); + } + } + - can_id: ${CB_BATTERY_STATE} # 0x355 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + //ESP_LOGI(cbitem.tag().c_str(), "%s", cbitem.to_string().c_str()); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + auto value = static_cast((x[1] << 8) + x[0]); + id(battery_soc).publish_state(value); + value = static_cast((x[3] << 8) + x[2]); + id(battery_soh).publish_state(value); + cbitem.setpublish(false); + } + } + - can_id: ${CB_BATTERY_STATUS} # 0x356 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + float value = 0.01 * static_cast((x[1] << 8) + x[0]); // unit = 0.01V Voltage of single module or average module voltage of system + id(battery_system_voltage).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A Module or system total current + id(battery_system_current).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1°C + id(battery_average_cell_temperature).publish_state(value); + cbitem.setpublish(false); + } + } + - can_id: ${CB_BATTERY_FAULT} # 0x359 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + char buffer[16]; + bool publish = false; + uint8_t protection1 = x[0]; + uint8_t protection2 = x[1]; + uint8_t alarm1 = x[2]; + uint8_t alarm2 = x[3]; + uint8_t module_numbers = x[4]; + char ch5 = x[5]; + char ch6 = x[6]; + id(battery_discharge_over_current).publish_state(protection1 & 0x80); + id(battery_cell_under_temperature).publish_state(protection1 & 0x10); + id(battery_cell_over_temperature).publish_state(protection1 & 0x08); + id(battery_cell_or_module_under_voltage).publish_state(protection1 & 0x04); + id(battery_cell_or_module_over_voltage).publish_state(protection1 & 0x02); + id(battery_system_error).publish_state(protection2 & 0x8); + id(battery_charge_over_current).publish_state(protection2 & 0x01); + id(battery_discharge_high_current).publish_state(alarm1 & 0x80); + id(battery_cell_low_temperature).publish_state(alarm1 & 0x10); + id(battery_cell_high_temperature).publish_state(alarm1 & 0x08); + id(battery_cell_or_module_low_voltage).publish_state(alarm1 & 0x04); + id(battery_cell_or_module_high_voltage).publish_state(alarm1 & 0x02); + id(battery_internal_communication_fail).publish_state(alarm2 & 0x8); + id(battery_charge_high_current).publish_state(alarm2 & 0x01); + snprintf(buffer, sizeof(buffer), "%d %c%c", module_numbers, ch5, ch6); + id(battery_module_numbers).publish_state(buffer); + cbitem.setpublish(false); + } + } + - can_id: ${CB_BATTERY_REQUEST_FLAG} # 0x35C + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + uint8_t request_flag = x[0]; + id(battery_charge_enable).publish_state(request_flag & 0x80); + id(battery_discharge_enable).publish_state(request_flag & 0x40); + bool request_force_charge1 = request_flag & 0x20; + bool request_force_charge2 = request_flag & 0x10; + bool request_full_charge = request_flag & 0x08; + id(battery_request_force_charge1).publish_state(request_force_charge1); + id(battery_request_force_charge2).publish_state(request_force_charge2); + id(battery_request_full_charge).publish_state(request_full_charge); + if(request_force_charge1) { + ESP_LOGW("Battery", "Request force charge I. Designed for when inverter allows battery to shut down, and able to wake battery up to charge it"); + } + if(request_force_charge2) { + ESP_LOGW("Battery", "Request force charge II. Designed for when inverter doesn`t want battery to shut down, able to charge battery before shut down to avoid low energy."); + } + if(request_full_charge) { + ESP_LOGW("Battery", "Request full charge. Suggest inverter to charge the battery using grid."); + } + cbitem.setpublish(false); + } + } + - can_id: ${CB_BATTERY_MANUFACTURER} # 0x35E + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto cbitem = cbf_store_pylon(id(can1_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(cbitem); + if(publish) { + std::string str(x.begin(), x.end()); + id(battery_manufacturer).publish_state(str); + cbitem.setpublish(false); + } + } + +switch: + # if geyser is disabled, it will not be energised + - platform: template + device_class: switch + id: geyser_disable + name: "Geyser Disable" + icon: "mdi:water-boiler" + lambda: |- + return id(gb_geyser_disable); + turn_on_action: + - lambda: |- + id(gb_geyser_disable) = true; + ESP_LOGI("geyser", "Disabling geyser."); + + turn_off_action: + - lambda: |- + id(gb_geyser_disable) = false; + ESP_LOGI("geyser", "Enabling geyser."); + optimistic: False + + # in economy mode, geyser is only switched on when it can be powered by solar only, i.e. without using mains + - platform: gpio + pin: + number: GPIO36 + inverted: true + mode: + input: true + pullup: false # external pullup +# filters: +# - delayed_off: 100ms + id: economy_mode # mode_select_switch + name: "Economy Mode" # "Mode Select" + icon: "mdi:beach" + + - platform: restart + name: "${name} Restart" + id: "restart_switch" + + - platform: gpio + id: reset_energy_counters + pin: + number: GPIO2 + inverted: true + mode: + input: true + pullup: true + name: "Reset Energy Counters" + disabled_by_default: True + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + then: +# - sensor.integration.reset: geyser_energy +# - sensor.integration.reset: plugs_energy +# - sensor.integration.reset: mains_energy +# - sensor.integration.reset: lights_energy +# - sensor.integration.reset: generated_energy +# - sensor.integration.reset: house_energy_usage +# - sensor.integration.reset: energy_loss + - lambda: |- + auto currenttime = id(time_source).now(); + if(currenttime.is_valid()) { + id(energy_counters_reset_time) = currenttime.timestamp; + } + else { + ESP_LOGW("reset_energy_counters", "Time source invalid. Reset time not saved!"); + } + + - platform: gpio + pin: + number: GPIO16 + inverted: false + mode: output + id: geyser_relay + name: "Geyser Relay" + icon: "mdi:water-thermometer" + restore_mode: ALWAYS_OFF + on_turn_on: + - lambda: |- + ESP_LOGI("geyser", "Geyser Relay turned on. Relay status of flag: %s set to on", id(geyser_relay_status) ? "on" : "off" ); + id(geyser_relay_status) = true; // only set to false by other sensor / script to include hysteresis and thus avoid relay chattering + on_turn_off: + - lambda: |- + ESP_LOGI("geyser", "Geyser Relay turned off"); + + - platform: gpio + pin: + number: GPIO17 + inverted: true + mode: output + id: pool_relay + name: "Pool Relay" + icon: "mdi:pool" + restore_mode: ALWAYS_OFF + on_turn_on: + - delay: 30s # rapid on and off states can burn-out motor + - lambda: |- + //id(pool_relay_status) = true; // only set to false by other sensor / script to include hysteresis and thus avoid relay chattering + ESP_LOGI("pool", "Pool Relay turned on"); + on_turn_off: + - lambda: |- + ESP_LOGI("pool", "Pool Relay turned off"); + +tlc59208f_ext: + address: 0x20 + id: tlc59208f_1 + i2c_id: bus_b + reset_pin: + number: GPIO25 + mode: + output: true + pullup: true output: - platform: ledc @@ -433,8 +1033,8 @@ output: channel: 5 tlc59208f_id: 'tlc59208f_1' id: led5 - - platform: tlc59208f_ext + - platform: tlc59208f_ext channel: 6 tlc59208f_id: 'tlc59208f_1' id: led6 @@ -443,55 +1043,1879 @@ output: channel: 7 tlc59208f_id: 'tlc59208f_1' id: led7 - + light: - platform: monochromatic output: led0 name: "LED Geyser Temperature 0" id: led_geyser_temp0 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED BLUE on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED BLUE off"); - platform: monochromatic output: led1 name: "LED Geyser Temperature 1" id: led_geyser_temp1 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED GREEN on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED GREEN off"); - platform: monochromatic output: led2 name: "LED Geyser Temperature 2" id: led_geyser_temp2 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED YELLOW on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED YELLOW off"); - platform: monochromatic output: led3 name: "LED Geyser Temperature 3" id: led_geyser_temp3 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED YELLOW2 on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED YELLOW2 off"); - platform: monochromatic output: led4 name: "LED Geyser Temperature 4" id: led_geyser_temp4 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED ORANGE on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED ORANGE off"); - platform: monochromatic output: led5 name: "LED Geyser Temperature 5" id: led_geyser_temp5 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED ORANGE2 on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED ORANGE2 off"); - platform: monochromatic output: led6 name: "LED Geyser Temperature 6" id: led_geyser_temp6 default_transition_length: 20ms - + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED RED on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED RED off"); - platform: monochromatic output: led7 name: "LED Geyser Temperature 7" id: led_geyser_temp7 default_transition_length: 20ms + on_turn_on: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED RED2 on"); + on_turn_off: + - lambda: |- + ESP_LOGV("geyser", "Geyser Temperature LED RED2 off"); + - platform: monochromatic + output: led_inverter_battery_low + name: "LED Inverter Battery Low" + id: light_inverter_battery_low + default_transition_length: 20ms + on_turn_on: + - lambda: |- + ESP_LOGI("battery", "Battery Low"); +# on_turn_off: +# - lambda: |- +# ESP_LOGI("battery", "Battery OK"); + +binary_sensor: + - platform: status + # Status platform provides a connectivity sensor + name: "Status" + device_class: connectivity + + - platform: gpio + pin: + number: GPIO39 + inverted: false + mode: + input: true + pullup: false # external pullup + filters: + - delayed_off: 50ms + id: inverter_battery_charge_state + name: "Inverter Battery Charge" + device_class: battery + # on_press: + # then: + # - light.turn_on: + # id: light_inverter_battery_low + # brightness: 100% + # on_release: + # then: + # - light.turn_off: + # id: light_inverter_battery_low + + - platform: template + id: geyser_heating + name: "Geyser Heating" + lambda: |- + return id(geyser_current).state > 10; + device_class: heat + + - platform: template + id: mains_supply + name: "Mains Supply" + lambda: |- + return /* id(mains_voltage_adc).state > 180 || */ id(inv1_ac_input_voltage).state > 200 || id(inv2_ac_input_voltage).state > 200; // minimum acceptable voltage is 200; inverters more accurate for now + device_class: power + + - platform: analog_threshold + id: inverter1_2_overload + name: "Inverter 1 & 2 Overload" + sensor_id: inverter1_2_output_power + #threshold setting applies hysteresis taking geyser load that was removed into account + threshold: + upper: 10.0 + lower: 6.9 + device_class: power + on_state: + then: + - lambda: |- + ESP_LOGI("inverter", "Inverter 1 & 2 are being overloaded. Heavy loads should be switched off."); + # - switch.turn_off: geyser_relay + on_release: + then: + - lambda: |- + ESP_LOGI("inverter", "Overload is cleared."); + + - platform: template + id: is_public_holiday + name: "Public Holiday" + lambda: |- + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + int month = time_obj.month; + int day_of_month = time_obj.day_of_month; + int i = 0; + while(i < 12 && (id(public_holidays)[i][0] != month || id(public_holidays)[i][1] != day_of_month)) { + // ESP_LOGI("geyser", "%d ########### holiday check!: %d/%d ###########", i, id(holidays)[i][0], id(holidays)[i][1]); + i++; + } + // ESP_LOGI("geyser", "%d ########### Holiday = %d: %d/%d ###########", i, i < 12, id(holidays)[i][0], id(holidays)[i][1]); + return (i < 12); + } + return false; + + - platform: template + id: is_school_holiday + name: "School Holiday" + lambda: |- + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + int month = time_obj.month; + int day_of_month = time_obj.day_of_month; + for(int i = 0; i < ${MAX_SCHOOL_HOLIDAY_PERIODS}; i++) { + int startmonth = id(school_holidays)[i][0][0]; + int endmonth = id(school_holidays)[i][1][0]; + if(month >= startmonth && month <= endmonth) { + int startday = id(school_holidays)[i][0][1]; + int endday = id(school_holidays)[i][1][1]; + if(day_of_month >= startday && day_of_month <= endday) { + return true; + } + } + } + } + return false; + +# SOLAR BATTERY + - platform: template + id: battery_discharge_over_current + name: "Battery Discharge Over Current" + device_class: problem + - platform: template + id: battery_cell_under_temperature + name: "Battery Cell Under Temperature" + device_class: problem + - platform: template + id: battery_cell_over_temperature + name: "Battery Cell Over Temperature" + device_class: problem + - platform: template + id: battery_cell_or_module_under_voltage + name: "Battery Under Voltage" + device_class: problem + - platform: template + id: battery_cell_or_module_over_voltage + name: "Battery Over Voltage" + device_class: problem + - platform: template + id: battery_system_error + name: "Battery System Error" + device_class: problem + - platform: template + id: battery_charge_over_current + name: "Battery Charge Over Current" + device_class: problem + - platform: template + id: battery_discharge_high_current + name: "Battery Discharge High Current" + device_class: problem + - platform: template + id: battery_cell_low_temperature + name: "Battery Low Temperature" + device_class: problem + - platform: template + id: battery_cell_high_temperature + name: "Battery High Temperature" + device_class: problem + - platform: template + id: battery_cell_or_module_low_voltage + name: "Battery Low Voltage" + device_class: problem + - platform: template + id: battery_cell_or_module_high_voltage + name: "Battery High Voltage" + device_class: problem + - platform: template + id: battery_internal_communication_fail + name: "Battery Communication Fail" + device_class: problem + - platform: template + id: battery_charge_high_current + name: "Battery Charge High Current" + device_class: problem + - platform: template + id: battery_charge_enable + name: "Battery Charge Enable" + #device_class: battery_charging + - platform: template + id: battery_discharge_enable + name: "Battery Discharge Enable" + #device_class: battery_charging + - platform: template + id: battery_request_force_charge1 + name: "Battery Request Force Charge 1" + # device_class: battery_charging + - platform: template + id: battery_request_force_charge2 + name: "Battery Request Force Charge 2" + # device_class: battery_charging + - platform: template + id: battery_request_full_charge + name: "Battery Request Full Charge " + # device_class: battery_charging + - platform: template + id: battery_charging + name: "Battery Charging" + device_class: battery_charging + lambda: "return id(battery_system_current).state > 0;" +# +# Inverter 1 + - platform: template + name: "Inv1 Battery Connected" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x8000; + + - platform: template + name: "Inv1 Line Normal" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x4000; + + - platform: template + name: "Inv1 PV Input Normal" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x2000; + + - platform: template + name: "Inv1 Load Connect Allowed" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x1000; + + - platform: template + name: "Inv1 PV MPPT Working" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x0080; + + - platform: template + name: "Inv1 Load Connected" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x0040; + + - platform: template + name: "Inv1 Power Flow Version Supported" + # device_class: problem + lambda: |- + return id(g_inv1_power_flow) & 0x0001; + + - platform: template + name: "Inv1 Battery Charging" + device_class: battery_charging + lambda: |- + int battery_flow = (id(g_inv1_power_flow) >> 10) & 3; + return battery_flow & 0x01; + + - platform: template + name: "Inv1 Battery Discharging" + # device_class: battery_charging + lambda: |- + int battery_flow = (id(g_inv1_power_flow) >> 10) & 3; + return battery_flow & 0x02; + + - platform: template + name: "Inv1 Draw Power from Line" + lambda: |- + int line_flow = (id(g_inv1_power_flow) >> 8) & 3; + return line_flow & 0x01; + + - platform: template + name: "Inv1 Feed Power to Line" + lambda: |- + int line_flow = (id(g_inv1_power_flow) >> 8) & 3; + return line_flow & 0x10; + +# Inverter 2 + - platform: template + name: "Inv2 Battery Connected" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x8000; + + - platform: template + name: "Inv2 Line Normal" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x4000; + + - platform: template + name: "Inv2 PV Input Normal" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x2000; + + - platform: template + name: "Inv2 Load Connect Allowed" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x1000; + + - platform: template + name: "Inv2 PV MPPT Working" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x0080; + + - platform: template + name: "Inv2 Load Connected" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x0040; + + - platform: template + name: "Inv2 Power Flow Version Supported" + # device_class: problem + lambda: |- + return id(g_inv2_power_flow) & 0x0001; + + - platform: template + name: "Inv2 Battery Charging" + device_class: battery_charging + lambda: |- + int battery_flow = (id(g_inv2_power_flow) >> 10) & 3; + return battery_flow & 0x01; + + - platform: template + name: "Inv2 Battery Discharging" + # device_class: battery_charging + lambda: |- + int battery_flow = (id(g_inv2_power_flow) >> 10) & 3; + return battery_flow & 0x02; + + - platform: template + name: "Inv2 Draw Power from Line" + lambda: |- + int line_flow = (id(g_inv2_power_flow) >> 8) & 3; + return line_flow & 0x01; + + - platform: template + name: "Inv2 Feed Power to Line" + lambda: |- + int line_flow = (id(g_inv2_power_flow) >> 8) & 3; + return line_flow & 0x10; + + - platform: template + name: "Solar Surplus" + id: solar_surplus + lambda: |- + double sun_elevation = id(sun_sensor).elevation(); + float battery_level = id(battery_soc).state; + bool sun_high_enough = sun_elevation >= id(sun_elevation_minimum); + bool battery_full = battery_level == 100; + bool battery_getting_full = battery_level >= 96 && id(battery_charging).state; + bool surplus = sun_high_enough && (battery_full || battery_getting_full); + //ESP_LOGI("solar", "Solar Power surplus? : %s", surplus ? "Yes" : "No"); + return surplus; + + - platform: template + name: "Heating Enabled" + id: heating_enabled + lambda: |- + double sun_elevation = id(sun_sensor).elevation(); + float battery_level = id(battery_soc).state; + bool sun_high_enough = sun_elevation >= id(sun_elevation_minimum); + bool battery_ok = battery_level >= 95 && id(battery_system_current).state > -20; // if battery level >= 95% we can live with 20A (~1.1kW) being used from battery (cloudy conditions) + bool enabled = sun_high_enough && battery_ok; + //ESP_LOGI("solar", "Heating enabled? : %s", enabled ? "Yes" : "No"); + return enabled; + +#ads131m08: +# id: highres_adc +# cs_pin: GPIO5 +# drdy_pin: GPIO10 +# reference_voltage: 1.25 + +sensor: +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 0 +# name: "ADS Channel 0 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 1 +# name: "ADS Channel 1 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 2 +# name: "ADS Channel 2 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 3 +# name: "ADS Channel 3 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 4 +# name: "ADS Channel 4 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 5 +# name: "ADS Channel 5 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 6 +# name: "ADS Channel 6 Voltage" +# - platform: ads131m08 +# ads131m08_id: highres_adc +# channel: 7 +# name: "ADS Channel 7 Voltage" + + - platform: debug + free: + name: "Heap Free" + block: + name: "Heap Max Block" + loop_time: + name: "Loop Time" + cpu_frequency: + name: "CPU Frequency" + + # NB! Keep all ads1115 sample rates the same. Update intervals should be more than or equal to 1/sample_rate + - platform: ads1115_int + #resolution: 12_BITS + multiplexer: 'A0_A1' + gain: 2.048 # 4.096 + ads1115_id: ads1115_48 + sample_rate: 860 # 475 #860 + state_class: measurement + device_class: current + accuracy_decimals: 8 + name: "ADC House Current" + id: power_outlets_current + unit_of_measurement: "A" + icon: "mdi:current" + # update_interval: 8ms #5ms + #filters: + ## - offset: 0.0002 + # - lambda: return x * x; + # - sliding_window_moving_average: + # window_size: 1250 #1250 #5000 + # send_every: 208 #208 #416 + # send_first_at: 208 #208 #416 + # - lambda: return sqrt(x); + # - multiply: 555 #95 #88.44 + # - offset: 0.0 #-0.2 + # - lambda: |- + # if(abs(x) < 0.1) + # return 0.0; + # return x; + + # ads1115_48 + - platform: ads1115_int + multiplexer: 'A2_A3' + gain: 2.048 # 4.096 + ads1115_id: ads1115_48 + name: "ADC Geyser Current" + id: geyser_current + sample_rate: 860 #860 + state_class: measurement + device_class: current + accuracy_decimals: 8 + unit_of_measurement: "A" + icon: "mdi:current" + filters: + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 625 #1250 #5000 + send_every: 104 #208 #416 + send_first_at: 104 #208 #416 + - lambda: return sqrt(x); + - multiply: 100 #92.1 #91.1 #88.44 + - offset: 0.0 #-0.2 + # - lambda: |- + # if(abs(x) < 0.1) + # return 0.0; + # return x; + on_value_range: + - below: 5.0 + then: + - lambda: |- + ESP_LOGI("geyser", "No geyser current detected. Geyser not heating."); + - above: 5.0 + then: + - lambda: |- + ESP_LOGI("geyser", "Geyser current detected. Geyser was energised."); + + # ads1115_49 + - platform: ads1115_int + multiplexer: A0_A1 + gain: 2.048 # 4.096 + ads1115_id: ads1115_49 + name: "ADC Lights Current" + id: lights_current + sample_rate: 860 + # update_interval: 10ms + # id: lights_current_adc + state_class: measurement + device_class: current + accuracy_decimals: 8 + unit_of_measurement: "A" + icon: "mdi:current" + filters: + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 625 #1250 #5000 + send_every: 104 #208 #416 + send_first_at: 104 #208 #416 + - lambda: return sqrt(x); + - multiply: 100 #92.1 #91.1 #88.44 + - offset: 0.0 #-0.2 + # - lambda: |- + # if(abs(x) < 0.1) + # return 0.0; + # return x; + + - platform: ads1115_int + multiplexer: 'A2_A3' + gain: 2.048 # 4.096 + ads1115_id: ads1115_49 + name: "ADC Mains Current" + id: mains_current + sample_rate: 860 #475 + state_class: measurement + device_class: current + accuracy_decimals: 8 + unit_of_measurement: "A" + icon: "mdi:current" + #update_interval: 8ms #5ms + #filters: + ## - offset: 0.0002 + # - lambda: return x * x; + # - sliding_window_moving_average: + # window_size: 1250 #1250 #5000 + # send_every: 208 #208 #416 + # send_first_at: 208 #208 #416 + # - lambda: return sqrt(x); + # - multiply: 95 #88.44 + # - offset: 0.0 #-0.2 + # - lambda: |- + # if(abs(x) < 0.1) + # return 0.0; + # return x; + +# + # ads1115_4A + # Inverter voltage sensor + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC Mains Voltage" + id: mains_voltage_adc + unit_of_measurement: "V" + accuracy_decimals: 8 + icon: "mdi:flash" + multiplexer: A0_GND + gain: 2.048 # 4.096 + #update_interval: 8ms #5ms #23ms + device_class: voltage + state_class: measurement +# filters: +# - offset: -2.048 #-2.04794027 # 0.0131 +# - lambda: return x * x; +# - sliding_window_moving_average: +# window_size: 1250 #1250 +# send_every: 208 +# send_first_at: 208 +# - lambda: return sqrt(x); +# - multiply: 766.6670 # 930 #650 + #- lambda: |- + # if(abs(x) < 10) + # return 0; + # return x;# + + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC Spare1 Voltage" + unit_of_measurement: "V" + accuracy_decimals: 8 + icon: "mdi:flash" + multiplexer: A1_GND + gain: 2.048 # 4.096 + #update_interval: 8ms #5ms #23ms + device_class: voltage + state_class: measurement + + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC House Voltage" + id: inverter_output_voltage_adc + unit_of_measurement: "V" + accuracy_decimals: 8 + icon: "mdi:flash" + multiplexer: A2_GND + gain: 2.048 # 4.096 + device_class: voltage + state_class: measurement +# filters: +# - offset: -2.048 #-2.0491 #4.096 #0.0065 +# - lambda: return x * x; +# - sliding_window_moving_average: +# window_size: 1250 #625 #1250 +# send_every: 208 #104 +# send_first_at: 208 #104 #416 +# - lambda: return sqrt(x); +# - multiply: 766.6670 # 930 #650 + #- lambda: |- + # if(abs(x) < 20) + # return 0; + # return x; + + + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC Spare2 Voltage" + unit_of_measurement: "V" + accuracy_decimals: 8 + icon: "mdi:flash" + multiplexer: A3_GND + gain: 2.048 # 4.096 + #update_interval: 8ms #5ms #23ms + device_class: voltage + state_class: measurement + + +# +## # 30A clamp +## - platform: ct_clamp +## sensor: geyser_current_adc +## id: geyser_current +## name: "Geyser Current" +## update_interval: 2s +## sample_duration: 2000ms #15000ms +## state_class: measurement +## device_class: current +## filters: +## # burden resistor is 62Ω in parallel with 33Ω = 21.54Ω +## # multiplier should be 1860/21.54 = x86.35 +## - multiply: 88.51 # real world +## - lambda: |- +## if(x < 0.25) +## return 0.0; +## return x; +## on_value_range: +## - below: 0.5 +## then: +## - lambda: |- +## ESP_LOGI("geyser", "Geyser lost power."); +## - above: 0.5 +## then: +## - lambda: |- +## ESP_LOGI("geyser", "Geyser was energised."); +# +## # 30A clamp +## - platform: ct_clamp +## sensor: lights_current_adc +## id: lights_current +## name: "Lights Current" +## update_interval: 1s +## sample_duration: 1s #15000ms +## state_class: measurement +## device_class: current +## filters: +## # burden resistor is 62Ω in parallel with 33Ω = 21.54Ω +## # multiplier should be 1860/21.54 = x86.35 +## - multiply: 88.44 # real world +## - lambda: |- +## if(x < 0.25) +## return 0.0; +## return x; +# +## # 100A clamp +## - platform: ct_clamp +## sensor: mains_current_adc +## id: mains_current +## name: "Mains Current" +## update_interval: 1s +## sample_duration: 1s #15000ms +## state_class: measurement +## device_class: current +## filters: +## # burden resistor is 22Ω +## # multiplier should be 2000/22 = x90.9 +## - multiply: 90.25 # real world +## - lambda: |- +## if(x < 0.25) +## return 0.0; +## return x; +## +## # 100A clamp +## - platform: ct_clamp +## sensor: power_outlets_current_adc +## id: power_outlets_current +## name: "Plugs Supply Current" +## update_interval: 1s +## sample_duration: 1s #15000ms +## state_class: measurement +## device_class: current +## filters: +## # burden resistor is 22Ω +## # multiplier should be 2000/22 = x90.9 +## - multiply: 91.14 # real world +## - lambda: |- +## if(x < 0.25) +## return 0.0; +## return x; +# +# - platform: template +# id: calibrate_lights +# name: "AAA Lights A" +# lambda: |- +# return id(lights_current).state; +# state_class: measurement +# device_class: current +# accuracy_decimals: 8 +# update_interval: 1s +# +# - platform: template +# id: calibrate_mains +# name: "AAA Mains A" +# lambda: |- +# return id(mains_current).state; +# state_class: measurement +# device_class: current +# accuracy_decimals: 8 +# update_interval: 1s +# +# - platform: template +# id: calibrate_mains_v +# name: "AAA Mains V" +# lambda: |- +# return id(mains_voltage_adc).state; +# state_class: measurement +# device_class: voltage +# accuracy_decimals: 8 +# update_interval: 1s +# +# - platform: template +# id: calibrate_plugs +# name: "AAA Plugs A" +# lambda: |- +# return id(power_outlets_current).state; +# state_class: measurement +# device_class: current +# accuracy_decimals: 8 +# update_interval: 1s +# +# - platform: template +# id: calibrate_plugs_V +# name: "AAA Plugs V" +# lambda: |- +# return id(inverter_output_voltage_adc).state; +# state_class: measurement +# device_class: voltage +# accuracy_decimals: 8 +# update_interval: 1s +# +# - platform: template +# id: calibrate_geyser +# name: "AAA Geyser A" +# lambda: |- +# return id(geyser_current).state; +# state_class: measurement +# device_class: current +# accuracy_decimals: 8 +# update_interval: 1s +# +# for now we use a template until we get a voltage sensor + - platform: template + id: mains_voltage + name: "Mains Voltage" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "V" + lambda: |- + return 230.0; + update_interval: 2s + device_class: voltage + state_class: measurement + +# for now we use a template until we get a voltage sensor + - platform: template + id: lights_voltage + name: "Lights Voltage" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "V" + lambda: |- + return 230.0; + update_interval: 2s + device_class: voltage + state_class: measurement + +# for now we use a template until we get a voltage sensor + - platform: template + id: inverter1_2_output_voltage + name: "Inverter 1 & 2 Output Voltage" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "V" + lambda: |- + return 230.0; + update_interval: 2s + device_class: voltage + state_class: measurement + + - platform: template +# # if no current is flowing to estimate heating time + id: geyser_element_power + unit_of_measurement: "W" + name: "Geyser Element Power" + lambda: |- + return 3000.0; + device_class: power + state_class: measurement + + - platform: template + id: inverter1_2_output_current + name: "Inverter 1 & 2 Output Current" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "A" + lambda: |- + return id(power_outlets_current).state + id(geyser_current).state; + update_interval: 2s + device_class: current + state_class: measurement +# + - platform: template + id: inverter1_2_output_power + name: "Inverter 1 & 2 Output Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + return 0.001 * (id(inverter1_2_output_voltage).state * id(inverter1_2_output_current).state); + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: geyser_power + name: "Geyser Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + filters: + - filter_out: nan + lambda: |- + return 0.001 * id(inverter1_2_output_voltage).state * id(geyser_current).state; + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: power_outlets_power + name: "Plugs Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + filters: + - filter_out: nan + lambda: |- + return 0.001 * (id(inverter1_2_output_voltage).state * id(power_outlets_current).state); + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: lights_power + name: "Lights Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + return 0.001 * (id(lights_voltage).state * id(lights_current).state); + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: total_inverter_output + name: "Total Inverter Output" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + return id(lights_power).state + id(power_outlets_power).state + id(geyser_power).state; + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: mains_power + name: "Mains Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + return 0.001 * (id(mains_voltage).state * id(mains_current).state); + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: generated_power + name: "Generated Power" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + auto power = id(total_inverter_output).state - id(mains_power).state; + if(power < 0) + return 0.0; + return power; + update_interval: 2s + device_class: power + state_class: measurement + + - platform: template + id: power_loss + name: "Power Loss" +# icon: mdi:flash + accuracy_decimals: 2 + unit_of_measurement: "kW" + lambda: |- + auto power = id(total_inverter_output).state - id(mains_power).state; + if(power < 0) + return -power; + return 0.0; + update_interval: 2s + device_class: power + state_class: measurement +# +# - platform: homeassistant +# entity_id: input_number.geyser_target_temp +# id: geyser_target_temp + + - platform: homeassistant + entity_id: input_number.bottom_temperature + id: geyser_bottom_temp + + - platform: homeassistant + entity_id: input_number.top_temperature + id: geyser_top_temp + +###################################################################### + + - platform: total_daily_energy + name: 'Daily Geyser Energy' + id: daily_geyser_energy + power_id: geyser_power + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: 'Daily Plugs Energy' + id: daily_plugs_energy + power_id: power_outlets_power + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: 'Daily Mains Energy' + id: daily_mains_energy + power_id: mains_power + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: 'Daily Lights Energy' + id: daily_lights_energy + power_id: lights_power + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: 'Daily Generated Energy' + id: daily_generated_energy + power_id: generated_power + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: "Daily House Energy Usage" + id: daily_house_energy_usage + power_id: total_inverter_output + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: "Daily Energy Loss" + id: daily_energy_loss + power_id: power_loss + unit_of_measurement: 'kWh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + +# NB! this is in Watt-hours + - platform: total_daily_energy + name: 'Daily Battery Energy In' + id: daily_battery_energy_in + power_id: battery_power_in + unit_of_measurement: 'Wh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + + - platform: total_daily_energy + name: 'Daily Battery Energy Out' + id: daily_battery_energy_out + power_id: battery_power_out + unit_of_measurement: 'Wh' + state_class: total_increasing + device_class: energy + accuracy_decimals: 3 + +###################################################################### + +# monthly integration sensor + - platform: integration + name: 'Monthly Geyser Energy' + id: monthly_geyser_energy + sensor: geyser_power + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly Plugs Energy' + id: monthly_plugs_energy + sensor: power_outlets_power + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly Mains Energy' + id: monthly_mains_energy + sensor: mains_power + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly Lights Energy' + id: monthly_lights_energy + sensor: lights_power + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly Generated Energy' + id: monthly_generated_energy + sensor: generated_power + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly House Energy Usage' + id: monthly_house_energy_usage + sensor: total_inverter_output + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +# monthly integration sensor + - platform: integration + name: 'Monthly Energy Loss' + id: monthly_energy_loss + sensor: power_loss + time_unit: h + restore: true + state_class: total_increasing + device_class: energy + unit_of_measurement: 'kWh' + accuracy_decimals: 3 + +###################################################################### + + - platform: template + id: heating_loss + name: "Heat Loss (now)" + icon: mdi:thermometer + unit_of_measurement: "W" + lambda: |- + return id(g_heat_loss); + update_interval: 2s + + - platform: template + id: active_schedule_day + name: "Schedule Day" + icon: mdi:calendar-clock + accuracy_decimals: 0 + unit_of_measurement: "" + lambda: |- + auto time_obj = ESPTime::from_epoch_local(id(active_schedule_period)[0]); + return time_obj.day_of_week; + update_interval: 2s + + - platform: template + id: active_schedule_temp + name: "Schedule Temp" + icon: mdi:water-thermometer-outline + unit_of_measurement: "°C" + lambda: |- + return id(active_schedule_temperature); + update_interval: 2s + + - platform: template + id: heat_gained + name: "Heat gained" + icon: mdi:water-thermometer-outline + unit_of_measurement: "W" + lambda: |- + return id(g_heat_gained); + update_interval: 2s + + - platform: template + id: calculated_heat_loss + name: "Heat loss (est)" + icon: mdi:water-thermometer-outline + unit_of_measurement: "W" + lambda: |- + double dtemp = id(last_temp_diff); + if(dtemp < -100) + return 0; + return id(thermal_transmittance) * id(geyser_surface_area) * id(last_temp_diff); + update_interval: 2s + + - platform: template + id: last_geyser_top_temp + name: "Last temperature" + icon: mdi:water-thermometer-outline + unit_of_measurement: "°C" + lambda: |- + return id(last_geyser_top_temperature); + update_interval: 2s + device_class: temperature + state_class: measurement + + - platform: dallas_temp + address: 0x2e00000059db6928 + name: "Geyser Top Temperature" + id: geyser_top_temperature + update_interval: "60s" + resolution: 12 + one_wire_id: geyser_temperature_sensors + unit_of_measurement: "°C" + #icon: "mdi:water-thermometer" + device_class: "temperature" + state_class: "measurement" + accuracy_decimals: 1 + filters: + - filter_out: nan + # - sliding_window_moving_average: + # window_size: 120 # averages over 120 update intervals + # send_every: 60 # reports every 60 update intervals + + - platform: dallas_temp + address: 0x0b00000036f14d28 + name: "Geyser Bottom Temperature" + id: geyser_bottom_temperature + update_interval: "60s" + resolution: 12 + one_wire_id: geyser_temperature_sensors + unit_of_measurement: "°C" + #icon: "mdi:water-thermometer" + device_class: "temperature" + state_class: "measurement" + accuracy_decimals: 1 + filters: + - filter_out: nan + # - sliding_window_moving_average: + # window_size: 120 # averages over 120 update intervals + # send_every: 60 # reports every 60 update intervals + + - platform: dallas_temp + address: 0x6455a0d445e8f028 + name: "Ambient Temperature" + id: ambient_temperature + update_interval: "60s" + resolution: 12 + one_wire_id: geyser_temperature_sensors + unit_of_measurement: "°C" + #icon: "mdi:water-thermometer" + device_class: "temperature" + state_class: "measurement" + accuracy_decimals: 1 + filters: + - filter_out: nan + # - sliding_window_moving_average: + # window_size: 120 # averages over 120 update intervals + # send_every: 60 # reports every 60 update intervals + + # Report wifi signal strength every 5 min if changed + - platform: wifi_signal + name: WiFi Signal + update_interval: 300s + filters: + - delta: 10% + # human readable uptime sensor output to the text sensor above + - platform: uptime + name: Uptime in Days + id: uptime_sensor_days + update_interval: 10s + on_raw_value: + then: + - text_sensor.template.publish: + id: uptime_human + state: !lambda |- + int seconds = round(id(uptime_sensor_days).raw_state); + int days = seconds / (24 * 3600); + seconds = seconds % (24 * 3600); + int hours = seconds / 3600; + seconds = seconds % 3600; + int minutes = seconds / 60; + seconds = seconds % 60; + auto days_str = std::to_string(days); + auto hours_str = std::to_string(hours); + auto minutes_str = std::to_string(minutes); + auto seconds_str = std::to_string(seconds); + return ( + (days ? days_str + "d " : "") + + (hours ? hours_str + "h " : "") + + (minutes ? minutes_str + "m " : "") + + (seconds_str + "s") + ).c_str(); + +# number of seconds since midnight +# - platform: template +# id: time_of_day +# name: "Time of day" +# accuracy_decimals: 0 +# unit_of_measurement: "s" +# lambda: |- +# auto currenttime = id(time_source).now(); +# ESPTime time_obj = currenttime; +# time_obj.second = 0; +# time_obj.minute = 0; +# time_obj.hour = 0; +# time_obj.recalc_timestamp_local(); +# return currenttime.timestamp - time_obj.timestamp; +# update_interval: 10s + +# SOLAR BATTERY + - platform: template + id: battery_level + name: "Battery Level" + accuracy_decimals: 0 + unit_of_measurement: "%" + state_class: measurement + device_class: battery + lambda: "{ return id(battery_soc).state; }" + - platform: template + id: battery_soc + name: "Battery SOC" + accuracy_decimals: 0 + unit_of_measurement: "%" + state_class: measurement + device_class: battery + - platform: template + id: battery_soh + name: "Battery Health" + accuracy_decimals: 0 + unit_of_measurement: "%" + state_class: measurement + device_class: battery + - platform: template + id: battery_system_voltage + name: "Battery Voltage" + accuracy_decimals: 2 + unit_of_measurement: "V" + state_class: measurement + device_class: voltage + - platform: template + id: battery_system_current + name: "Battery Current" + accuracy_decimals: 1 + unit_of_measurement: "A" + state_class: measurement + device_class: current + on_value: + then: + lambda: |- + float rate = 0.0; + float current = x; + // set charging rate + float current_limit = id(battery_charge_current_limit).state; + if(current > 0 && current_limit > 0) { + rate = 100 * current / current_limit; + } + id(battery_charging_rate).publish_state(rate); + + - platform: template + name: "Battery Power" + id: battery_power + unit_of_measurement: "W" + device_class: power + lambda: |- + double current = id(battery_system_current).state; + double power = current * id(battery_system_voltage).state; + // set battery power indicaters + if(current < 0) { + id(battery_power_out).publish_state(-power); + id(battery_power_in).publish_state(0); + } + else { + id(battery_power_in).publish_state(power); + id(battery_power_out).publish_state(0); + } + return power; + + - platform: template + id: battery_power_in + name: "Battery Power In" + accuracy_decimals: 2 + unit_of_measurement: "W" + device_class: power + state_class: measurement + - platform: template + id: battery_power_out + name: "Battery Power Out" + accuracy_decimals: 2 + unit_of_measurement: "W" + device_class: power + state_class: measurement + - platform: template + id: battery_average_cell_temperature + name: "Battery Cell Temperature" + accuracy_decimals: 1 + unit_of_measurement: "°C" + device_class: temperature + state_class: measurement + - platform: template + id: battery_charge_voltage_limit + name: "Battery Charge Voltage Limit" + accuracy_decimals: 1 + unit_of_measurement: "V" + state_class: measurement + device_class: voltage + - platform: template + id: battery_charge_current_limit + name: "Battery Charge Current Limit" + accuracy_decimals: 1 + unit_of_measurement: "A" + state_class: measurement + device_class: current + on_value: + then: + lambda: |- + float rate = 0.0; + float current = id(battery_system_current).state; + float current_limit = x; + if(current > 0 && current_limit > 0) { + rate = 100 * current / current_limit; + } + id(battery_charging_rate).publish_state(rate); + - platform: template + id: battery_discharge_current_limit + name: "Battery Discharge Current Limit" + accuracy_decimals: 1 + unit_of_measurement: "A" + state_class: measurement + device_class: current + # charging rate as a percentage of potential charging rate + - platform: template + name: "Battery Charging Rate" + id: battery_charging_rate + accuracy_decimals: 1 + unit_of_measurement: "%" + state_class: measurement + device_class: battery + #lambda: |- + # if(id(battery_system_current).state < 0) { + # return 0.0; + # } + # return 100 * id(battery_system_current).state / id(battery_charge_current_limit).state; +# +# Inverter 1 + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 SettingDataSn" + register_type: holding + address: ${Felicity_Inv_SettingDataSn} # 0x1100 + accuracy_decimals: 0 + value_type: U_WORD + register_count: 1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Fault Code" + register_type: holding + address: ${Felicity_Inv_FaultCode} # 0x1103 + value_type: U_WORD + register_count: 1 + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + id: inv1_power_flow_msg + register_type: holding + address: ${Felicity_Inv_PowerFlowMsg} # 0x1104 + value_type: U_WORD + register_count: 4 + lambda: |- + id(g_inv1_power_flow) = x; + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Battery Voltage" + register_type: holding + address: ${Felicity_Inv_BatteryVoltage} # 0x1108 + value_type: U_WORD + register_count: 1 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.01 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Battery Current" + register_type: holding + address: ${Felicity_Inv_BatteryCurrent} # 0x1109 + value_type: S_WORD + register_count: 1 + unit_of_measurement: "A" + device_class: current + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 BatteryPower" + id: inv1_batterypower + register_type: holding + address: ${Felicity_Inv_BatteryPower} # 0x110A + value_type: S_WORD + register_count: 7 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 AC Output Voltage" + register_type: holding + address: ${Felicity_Inv_ACOutputVoltage} # 0x1111 + value_type: U_WORD + register_count: 6 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 AC Input Voltage" + id: inv1_ac_input_voltage + register_type: holding + address: ${Felicity_Inv_ACInputVoltage} # 0x1117 + value_type: U_WORD + register_count: 2 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 AC Input Frequency" + id: inv1_ac_input_frequency + register_type: holding + address: ${Felicity_Inv_ACInputFrequency} # 0x1119 + value_type: U_WORD + register_count: 5 + unit_of_measurement: "Hz" + device_class: frequency + accuracy_decimals: 2 + filters: + - multiply: 0.01 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 AC Output Active Power" + id: inv1_ac_output_active_power + register_type: holding + address: ${Felicity_Inv_ACOutputActivePower} # 0x111E + value_type: S_WORD + register_count: 1 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 AC Output Apparent Power" + register_type: holding + address: ${Felicity_Inv_ACOutputApparentPower} # 0x111F + value_type: U_WORD + register_count: 1 + unit_of_measurement: "VA" + device_class: apparent_power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Load Percentage" + register_type: holding + address: ${Felicity_Inv_LoadPercentage} # 0x1120 + value_type: U_WORD + register_count: 6 + unit_of_measurement: "%" + device_class: battery + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 PV Input Voltage" + id: inv1_pv_input_voltage + register_type: holding + address: ${Felicity_Inv_PVInputVoltage} # 0x1126 + value_type: U_WORD + register_count: 4 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 PV Input Power" + id: inv1_pv_input_power + register_type: holding + address: ${Felicity_Inv_PVInputPower} # 0x112A + value_type: S_WORD + register_count: 1 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + ############### modbus device 2 ############### + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 SettingDataSn" + register_type: holding + address: ${Felicity_Inv_SettingDataSn} # 0x1100 + accuracy_decimals: 0 + value_type: U_WORD + register_count: 1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Fault Code" + register_type: holding + address: ${Felicity_Inv_FaultCode} # 0x1103 + value_type: U_WORD + register_count: 1 + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + id: inv2_power_flow_msg + register_type: holding + address: ${Felicity_Inv_PowerFlowMsg} # 0x1104 + value_type: U_WORD + register_count: 4 + lambda: |- + id(g_inv2_power_flow) = x; + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Battery Voltage" + register_type: holding + address: ${Felicity_Inv_BatteryVoltage} # 0x1108 + value_type: U_WORD + register_count: 1 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.01 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Battery Current" + register_type: holding + address: ${Felicity_Inv_BatteryCurrent} # 0x1109 + value_type: S_WORD + register_count: 1 + unit_of_measurement: "A" + device_class: current + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 BatteryPower" + id: inv2_batterypower + register_type: holding + address: ${Felicity_Inv_BatteryPower} # 0x110A + value_type: S_WORD + register_count: 7 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 AC Output Voltage" + register_type: holding + address: ${Felicity_Inv_ACOutputVoltage} # 0x1111 + value_type: U_WORD + register_count: 6 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 AC Input Voltage" + id: inv2_ac_input_voltage + register_type: holding + address: ${Felicity_Inv_ACInputVoltage} # 0x1117 + value_type: U_WORD + register_count: 2 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 AC Input Frequency" + register_type: holding + address: ${Felicity_Inv_ACInputFrequency} # 0x1119 + value_type: U_WORD + register_count: 5 + unit_of_measurement: "Hz" + device_class: frequency + accuracy_decimals: 2 + filters: + - multiply: 0.01 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 AC Output Active Power" + id: inv2_ac_output_active_power + register_type: holding + address: ${Felicity_Inv_ACOutputActivePower} # 0x111E + value_type: S_WORD + register_count: 1 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 AC Output Apparent Power" + register_type: holding + address: ${Felicity_Inv_ACOutputApparentPower} # 0x111F + value_type: U_WORD + register_count: 1 + unit_of_measurement: "VA" + device_class: apparent_power + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Load Percentage" + register_type: holding + address: ${Felicity_Inv_LoadPercentage} # 0x1120 + value_type: U_WORD + register_count: 6 + unit_of_measurement: "%" + device_class: battery + accuracy_decimals: 0 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 PV Input Voltage" + register_type: holding + address: ${Felicity_Inv_PVInputVoltage} # 0x1126 + value_type: U_WORD + register_count: 4 + unit_of_measurement: "V" + device_class: voltage + accuracy_decimals: 1 + filters: + - multiply: 0.1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 PV Input Power" + id: inv2_pv_input_power + register_type: holding + address: ${Felicity_Inv_PVInputPower} # 0x112A + value_type: S_WORD + register_count: 1 + unit_of_measurement: "W" + device_class: power + accuracy_decimals: 0 + + - platform: template + name: "PV Input Power" + id: pv_input_power + unit_of_measurement: "W" + device_class: power + lambda: |- + return id(inv1_pv_input_power).state + id(inv2_pv_input_power).state; + + - platform: template + name: "Battery Power Inv" + id: battery_power_inv + unit_of_measurement: "W" + device_class: power + lambda: |- + return id(inv1_batterypower).state + id(inv2_batterypower).state; + + - platform: template + name: "Local Generated Power" + id: local_generated_power + unit_of_measurement: "W" + device_class: power + lambda: |- + return id(inv1_pv_input_power).state + id(inv2_pv_input_power).state - id(inv1_batterypower).state - id(inv2_batterypower).state; + + - platform: template + name: "House Power Draw" + id: house_power_draw + unit_of_measurement: "W" + device_class: power + lambda: |- + return id(inv1_ac_output_active_power).state + id(inv2_ac_output_active_power).state; + + - platform: template + name: "Grid Power Draw" + id: grid_power_draw + unit_of_measurement: "W" + device_class: power + lambda: |- + return id(inv1_ac_output_active_power).state + id(inv2_ac_output_active_power).state + id(inv1_batterypower).state + id(inv2_batterypower).state - id(inv1_pv_input_power).state - id(inv2_pv_input_power).state; text_sensor: - platform: debug @@ -499,10 +2923,30 @@ text_sensor: name: "Device Info" reset_reason: name: "Reset Reason" + - platform: template - name: Uptime - id: uptime_human - icon: mdi:clock-start + id: calculated_heat_loss_text + name: "Heat loss (est)" + icon: mdi:clock + lambda: |- + char buffer[32]; + time_t start_time = id(heat_monitor_start); + ESPTime time_obj = ESPTime::from_epoch_local(start_time); + auto timestr = time_obj.strftime("%H:%M"); + double hl = id(calculated_heat_loss).state; + snprintf(buffer, sizeof(buffer), "%.1f", hl); + auto heat_loss_str = std::string(buffer); + return heat_loss_str + "@" + timestr; + update_interval: 10s + +# - platform: template +# id: module_time +# name: "Module time" +# icon: mdi:clock +# lambda: |- +# auto time_obj = id(time_source).now(); +# return time_obj.strftime("%Y-%m-%d %H:%M:%S"); +# update_interval: 1s # Expose WiFi information as sensors - platform: wifi_info @@ -511,25 +2955,404 @@ text_sensor: mac_address: name: Mac Address + - platform: template + id: active_schedule_start_text + name: "Schedule Start" + icon: mdi:calendar-clock + + - platform: template + id: active_schedule_end_text + name: "Schedule End" + icon: mdi:calendar-clock + + - platform: template + id: heating_start_text + name: "Heating Start" + icon: mdi:clock-start + + - platform: template + id: heating_time_text + name: "Heating Time" + icon: mdi:clock-time-eight-outline + + - platform: template + id: heating_end_text + name: "Heating End" + icon: mdi:clock-end + + - platform: template + id: energy_counters_reset_time_text + name: "Energy Reset @" + icon: mdi:clock + lambda: |- + auto ts = id(energy_counters_reset_time); + auto time_obj = ESPTime::from_epoch_local(ts); + return time_obj.strftime("%Y-%m-%d %H:%M:%S"); + + # human readable update text sensor from sensor:uptime + - platform: template + name: Uptime + id: uptime_human + icon: mdi:clock-start + + - platform: homeassistant + name: "Geyser Target Temp Time" + entity_id: input_datetime.geyser_target_temp_time + id: geyser_target_temp_time + + - platform: homeassistant + name: "Geyser Schedule" + entity_id: schedule.geyser_schedule + id: hass_geyser_schedule + +# SOLAR BATTERY + - platform: template + id: battery_manufacturer + name: "Battery Manufacturer" + - platform: template + id: battery_module_numbers + name: "Battery Module Numbers" + +## inverters + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 SerialNo" + id: inv1_serialno + register_type: holding + address: ${Felicity_Inv_SerialNo} # 0xF804 + response_size: 14 # should be 10, but absorbing extra four bytes + raw_encode: HEXBYTES + lambda: |- + char buffer[32]; + uint16_t sn0 = modbus_controller::word_from_hex_str(x, 0); + uint16_t sn1 = modbus_controller::word_from_hex_str(x, 2); + uint16_t sn2 = modbus_controller::word_from_hex_str(x, 4); + uint16_t sn3 = modbus_controller::word_from_hex_str(x, 6); + uint16_t sn4 = modbus_controller::word_from_hex_str(x, 8); + snprintf(buffer, sizeof(buffer), "%04d%04d%04d%04d%04d", sn0, sn1, sn2, sn3, sn4); + return std::string(buffer).substr(0, 14); + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Type" + id: inverter1_type + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_Type} # 0xF800 + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch (value) { + case 0x50: return std::string("High Frequency Inverter"); + default: return std::string("Unknown"); + } + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Sub Type" + id: inverter1_subtype + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_SubType} # 0xF801 + response_size: 6 # should be 2, but absorbing extra four bytes + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch (value) { + case 0x0204: return std::string("3024 (3000VA/24V)"); + case 0x0408: return std::string("5048 (5000VA/48V)"); + default: return std::string("Unknown"); + } + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 CPU1 F/W Version" + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_CPU1_FW_Version} # 0xF80B + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + return std::to_string(value); + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 CPU2 F/W Version" + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_CPU2_FW_Version} # 0xF80C + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + return std::to_string(value); + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Working Mode" + address: ${Felicity_Inv_WorkingMode} # 0x1101 + bitmask: 0 + register_type: holding + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch(value) { + case 0: return std::string("Power On"); + case 1: return std::string("Standby"); + case 2: return std::string("Bypass"); + case 3: return std::string("Battery"); + case 4: return std::string("Fault"); + case 5: return std::string("Line"); + case 6: return std::string("PV Charge"); + } + return std::string("Unknown"); + register_count: 1 + + - platform: modbus_controller + modbus_controller_id: modbus_device1 + name: "Inv1 Charge Mode" + address: ${Felicity_Inv_BatteryChargingStage} # 0x1102 + bitmask: 0 + register_type: holding + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch(value) { + case 0: return std::string("Idle"); + case 1: return std::string("Bulk"); + case 2: return std::string("Absorption"); + case 3: return std::string("Float"); + } + return std::string("Unknown"); + register_count: 1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 SerialNo" + id: inv2_serialno + register_type: holding + address: ${Felicity_Inv_SerialNo} # 0xF804 + response_size: 14 # should be 10, but absorbing extra four bytes + raw_encode: HEXBYTES + lambda: |- + char buffer[32]; + uint16_t sn0 = modbus_controller::word_from_hex_str(x, 0); + uint16_t sn1 = modbus_controller::word_from_hex_str(x, 2); + uint16_t sn2 = modbus_controller::word_from_hex_str(x, 4); + uint16_t sn3 = modbus_controller::word_from_hex_str(x, 6); + uint16_t sn4 = modbus_controller::word_from_hex_str(x, 8); + snprintf(buffer, sizeof(buffer), "%04d%04d%04d%04d%04d", sn0, sn1, sn2, sn3, sn4); + return std::string(buffer).substr(0, 14); + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Type" + id: inverter2_type + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_Type} # 0xF800 + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch (value) { + case 0x50: return std::string("High Frequency Inverter"); + default: return std::string("Unknown"); + } + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Sub Type" + id: inverter2_subtype + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_SubType} # 0xF801 + response_size: 6 # should be 2, but absorbing extra four bytes + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch (value) { + case 0x0204: return std::string("3024 (3000VA/24V)"); + case 0x0408: return std::string("5048 (5000VA/48V)"); + default: return std::string("Unknown"); + } + return x; + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 CPU1 F/W Version" + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_CPU1_FW_Version} # 0xF80B + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + return std::to_string(value); + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 CPU2 F/W Version" + bitmask: 0 + register_type: holding + address: ${Felicity_Inv_CPU2_FW_Version} # 0xF80C + response_size: 2 + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + return std::to_string(value); + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Working Mode" + address: ${Felicity_Inv_WorkingMode} # 0x1101 + bitmask: 0 + register_type: holding + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch(value) { + case 0: return std::string("Power On"); + case 1: return std::string("Standby"); + case 2: return std::string("Bypass"); + case 3: return std::string("Battery"); + case 4: return std::string("Fault"); + case 5: return std::string("Line"); + case 6: return std::string("PV Charge"); + } + return std::string("Unknown"); + register_count: 1 + + - platform: modbus_controller + modbus_controller_id: modbus_device2 + name: "Inv2 Charge Mode" + address: ${Felicity_Inv_BatteryChargingStage} # 0x1102 + bitmask: 0 + register_type: holding + raw_encode: HEXBYTES + lambda: |- + uint16_t value = modbus_controller::word_from_hex_str(x, 0); + switch(value) { + case 0: return std::string("Idle"); + case 1: return std::string("Bulk"); + case 2: return std::string("Absorption"); + case 3: return std::string("Float"); + } + return std::string("Unknown"); + register_count: 1 + script: - - id: set_indicators + - id: set_active_schedule + then: + - lambda: |- + auto currenttime = id(time_source).now(); + int dayofweek = currenttime.day_of_week; + ESPTime start_of_day = currenttime; + start_of_day.second = 0; + start_of_day.minute = 0; + start_of_day.hour = 0; + start_of_day.recalc_timestamp_local(); + time_t today_seconds = start_of_day.timestamp; + time_t now_seconds = currenttime.timestamp; + time_t seconds = now_seconds - today_seconds; + id(active_schedule_temperature) = 0; // temperature = 0 is regarded as an empty setting + auto future_endtime = 700000; // initialise to max value + int active_idx = 0; + int active_blk = 0; + bool active_holiday = false; + int d = 0; + auto day_seconds = today_seconds; + do { + int g_schedule_idx = 0; + id(get_geyser_mode).execute(g_schedule_idx); + auto day_schedule = id(g_schedule)[g_schedule_idx]; + // // debug start + // auto t_obj = ESPTime::from_epoch_local(day_seconds); + // t_obj.recalc_timestamp_local(); + // auto date = t_obj.strftime("%Y-%m-%d"); + // ESP_LOGI("geyser", "date: %s", date.c_str()); + // // debug end + int blk = 0; + do { + if(day_schedule[blk][0] > 0) { + auto endtime = day_schedule[blk][2] + day_seconds; + if(endtime > now_seconds) { + time_t seconds_to_endtime = endtime - now_seconds; + if(seconds_to_endtime < future_endtime) { + future_endtime = seconds_to_endtime; + active_idx = g_schedule_idx; + active_blk = blk; + } + } + } + } + while(++blk < ${HEATING_DAY_BLOCKS}); // second dimension of the g_schedule array + dayofweek = (dayofweek < 7) ? dayofweek++ : 1; + day_seconds += 86400; // next day + } + while(++d < ${GEYSER_MODES}); // first dimension of the g_schedule array + auto day_schedule = id(g_schedule)[active_idx]; + id(active_schedule_temperature) = static_cast(day_schedule[active_blk][0]) / ${HEATING_TEMP_SCALE}; + id(active_schedule_period)[0] = day_schedule[active_blk][1] + today_seconds; + id(active_schedule_period)[1] = day_schedule[active_blk][2] + today_seconds; + //ESP_LOGI("geyser", "3. day:%d, block:%d, schedule: %d / %d / %d (%d)", active_idx, active_blk, day_schedule[active_blk][0], day_schedule[active_blk][1], day_schedule[active_blk][2], today_seconds); + //for(int i = 0; i < 12; i++) { + // ESP_LOGI("geyser", "holiday: {%d, %d}", id(holidays)[i][0], id(holidays)[i][1]); + //} + + - id: get_geyser_mode parameters: - low_value: double - high_value: double + index: int& + then: + - lambda: |- + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + int dayofweek = time_obj.day_of_week; + index = id(is_school_holiday).state ? ${GM_SCHOOL_HOLIDAY} : id(is_public_holiday).state ? ${GM_PUBLIC_HOLIDAY} : (dayofweek == 1) ? ${GM_SUNDAY} : (dayofweek == 7) ? ${GM_SATURDAY} : ${GM_WORKDAY}; + } + else { + index = ${GM_WORKDAY}; // default + } + + - id: set_active_heating_timers + then: + - lambda: |- + id(calc_geyser_heating_values).execute(id(active_schedule_temperature)); + id(active_heating_time) = id(estimated_heating_time); + // set heating start and end + auto schedule_start = id(active_schedule_period)[0]; + auto schedule_end = id(active_schedule_period)[1]; + if(schedule_end > schedule_start) { + // normal heating period + id(active_heating_start) = schedule_start; + id(active_heating_end) = schedule_end; + } + else { + // target temperature period + id(active_heating_start) = schedule_start - (id(active_heating_time) > 0 ? id(active_heating_time) : 0); // start heating the estimated heating time before scheduled start time + id(active_heating_end) = schedule_start; // move end to start time + } + + - id: set_heat_indicators + parameters: + temp_bottom: double + temp_top: double then: - lambda: |- double led_on = 1.0; int led_count = 8; - double min_value = 0.7; - double max_value = 0.8; - if(low_value < min_value) { - low_value = min_value; - } - double step_size = (max_value - min_value) / led_count; - double bottom_point = (low_value - min_value) / step_size; + double min_temp = 30.0; + double max_temp = 70.0; + double step_size = (max_temp - min_temp) / led_count; + double bottom_point = (temp_bottom - min_temp) / step_size; int led_bot = static_cast(std::trunc(bottom_point)); double led_bot_int = 1.0 + led_bot - bottom_point; - double top_point = (high_value - min_value) / step_size; + double top_point = (temp_top - min_temp) / step_size; int led_top = static_cast(std::trunc(top_point)); double led_top_int = top_point - led_top; double led_0 = 0; @@ -609,6 +3432,971 @@ script: id(led_geyser_temp4).turn_on().set_brightness(led_4).perform(); id(led_geyser_temp5).turn_on().set_brightness(led_5).perform(); id(led_geyser_temp6).turn_on().set_brightness(led_6).perform(); - id(led_geyser_temp7).turn_on().set_brightness(led_7).perform(); - //ESP_LOGI("geyser","bot: %f, top: %f 0: %0.2f 1: %0.2f 2: %0.2f 3: %0.2f 4: %0.2f 5: %0.2f 6: %0.2f 7: %0.2f ", low_value, high_value, led_0, led_1, led_2, led_3, led_4, led_5, led_6, led_7); + id(led_geyser_temp7).turn_on().set_brightness(led_7).perform(); + //ESP_LOGI("geyser","bot: %f, top: %f 0: %0.2f 1: %0.2f 2: %0.2f 3: %0.2f 4: %0.2f 5: %0.2f 6: %0.2f 7: %0.2f ", temp_bottom, temp_top, led_0, led_1, led_2, led_3, led_4, led_5, led_6, led_7); + # do at 1 second intervals - check if geyser is on, and switch off if required + - id: reset_geyser_relay + then: + - if: + condition: + lambda: "return id(inverter_battery_charge_state).state || id(battery_soc).state < 60 ;" + then: + - light.turn_on: + id: light_inverter_battery_low + brightness: 100% + else: + - light.turn_off: + id: light_inverter_battery_low + - lambda: |- + const char tag[] = "geyser"; + if(id(gb_geyser_disable)) { + //ESP_LOGI(tag, "Geyser disabled. Switching it off."); + id(geyser_relay).turn_off(); + return; + } + bool battery_low = id(inverter_battery_charge_state).state || id(battery_soc).state < 60; + double sun_elevation = id(sun_sensor).elevation(); + bool sun_high_enough = sun_elevation >= id(sun_elevation_minimum); + double top_temp = id(geyser_top_temperature).state; + auto currenttime = id(time_source).now(); + if(currenttime.is_valid()) { + time_t now = currenttime.timestamp; + bool relay_on = id(geyser_relay).state; + //ESP_LOGD(tag, "Geyser heating is %s.", relay_on ? "on" : "off"); + if(relay_on) { + // GEYSER IS ENERGISED + // =================== + // if we have solar power and battery is almost full, geyser will remain on regardless + if(id(heating_enabled).state) { + ESP_LOGD(tag, "Geyser remained on at %f °C due battery charge and solar power. Battery charge: %0.1f%% Battery current: %0.1f%A" , top_temp, id(battery_soc).state, id(battery_system_current).state); + id(geyser_relay_status) = true; + } + else { + if(now > id(active_heating_end)) { + // past the scheduled heating end + id(geyser_relay_status) = false; + id(geyser_relay).turn_off(); + ESP_LOGI(tag, "Past the scheduled heating end at %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + // we will do nothing if water has heated a bit faster than calculated, unless the water is more than 'temp_overshoot_allowed' (0.25) degrees hotter than target temperature + if(id(estimated_heating_overshoot_time) <= 0) { + // we turn geyser off to save energy + id(geyser_relay_status) = false; + ESP_LOGI(tag, "Water temperature (%f) is at or above target of %f °C. of Heating done", top_temp, id(active_schedule_temperature)); + } + if(id(inverter1_2_overload).state) { + id(geyser_relay_status) = false; + ESP_LOGI(tag, "Overload condition. Temperature: %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + if(battery_low && !id(mains_supply).state) { + // inverter battery is low + id(geyser_relay_status) = false; + ESP_LOGI(tag, "Low inverter battery voltage. Temperature: %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + if(!id(mains_supply).state && !sun_high_enough) { + // sun is not high enough above horizon and mains supply is off + id(geyser_relay_status) = false; + ESP_LOGI(tag, "No mains and inadequate solar power. Temperature: %f °C. Heating start: %s, end: %s, time: %d, Sun: %f ° elevation", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time), sun_elevation); + } + // NB! we only call economy procedure if flag is set to leave geyser on + if(id(geyser_relay_status)) { + // set flag to switch off geyser if economy mode requires it to be switched off + id(economy_mode_set_geyser_relay).execute(relay_on, sun_high_enough); + } + } + if(!id(geyser_relay_status)) { + id(geyser_relay).turn_off(); + ESP_LOGI(tag, "Geyser was turned off at %f °C.", top_temp); + } + } + } + + # do at 10 second intervals or longer - check if geyser is off and switch on if required + - id: set_geyser_relay + then: + - if: + condition: + lambda: "return id(inverter_battery_charge_state).state || id(battery_soc).state < 60 ;" + then: + - light.turn_on: + id: light_inverter_battery_low + brightness: 100% + else: + - light.turn_off: + id: light_inverter_battery_low + - lambda: |- + const char tag[] = "geyser"; + if(id(gb_geyser_disable)) { + ESP_LOGI(tag, "Geyser disabled. Switching it off."); + id(geyser_relay).turn_off(); + return; + } + bool battery_low = id(inverter_battery_charge_state).state || id(battery_soc).state < 60; + double sun_elevation = id(sun_sensor).elevation(); + bool sun_high_enough = sun_elevation >= id(sun_elevation_minimum); + double top_temp = id(geyser_top_temperature).state; + auto currenttime = id(time_source).now(); + if(currenttime.is_valid()) { + time_t now = currenttime.timestamp; + bool relay_on = id(geyser_relay).state; + //ESP_LOGD(tag, "Geyser heating is turned %s.", (relay_on) ? "on" : "off"); + if(!relay_on) { + id(geyser_relay_status) = false; + // GEYSER IS NOT ENERGISED + // ======================= + // if we have a surplus of solar power, we will turn geyser on regardless + if(id(solar_surplus).state) { + ESP_LOGI(tag, "Geyser was turned on at %f °C due to surplus solar power. Battery charge: %0.1f%%", top_temp, id(battery_soc).state); + id(geyser_relay_status) = true; + id(geyser_relay).turn_on(); + } + else if(id(active_heating_time) <= 0 || now > id(active_heating_end)) { + // no more heat required OR we are past the scheduled heating end + id(geyser_relay_status) = false; // ensure geyser saved state is set to 'off' + } + else { + // heat is required and we are not past the scheduled heating end + if(now >= id(active_heating_start)) { + // we are at or past the scheduled start time for heating + // we will do a few checks to see if it is ok to turn the geyser on + if(id(inverter1_2_overload).state) { + ESP_LOGI(tag, "Geyser not turned on due to overload condition. Temperature: %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + else if(battery_low && !id(mains_supply).state) { + // inverter battery is low + ESP_LOGI(tag, "Geyser not turned on due to low inverter battery voltage. Temperature: %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + else if((!id(mains_supply).state) && !sun_high_enough) { + // sun is not high enough above horizon and mains supply is off + ESP_LOGI(tag, "Geyser not turned on due to no mains and inadequate solar power. Temperature: %f °C. Heating start: %s, end: %s, time: %d, Sun: %f° elevation", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time), sun_elevation); + } + else { + id(geyser_relay_status) = true; + } + // NB! we only call economy procedure if flag is set to switch geyser on + if(id(geyser_relay_status)) { + // leave geyser switched off if economy mode requires it to be switched off + id(economy_mode_set_geyser_relay).execute(relay_on, sun_high_enough); + } + if(id(geyser_relay_status)) { + id(geyser_relay).turn_on(); + ESP_LOGI(tag, "Geyser is turned on at %f °C. Heating start: %s, end: %s, time: %d", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time)); + } + } + } + } + } + + # set/reset geyser_relay_status variable if economy mode requires it + - id: economy_mode_set_geyser_relay + parameters: + relay_on: bool + sun_high_enough: bool + then: + - lambda: |- + if(id(economy_mode).state) { + // only set/reset geyser_relay_status here if economy mode is active + double top_temp = id(geyser_top_temperature).state; + const char tag[] = "geyser"; + if(relay_on) { + double available_solar_power = id(local_generated_power).state; + double geyser_power = id(geyser_element_power).state; + // GEYSER IS ENERGISED + // =================== + if(!sun_high_enough) { + id(geyser_relay_status) = false; + ESP_LOGI(tag, "Economy mode: sun not high enough, geyser to be turned off at %f °C. Heating start: %s, end: %s, time: %d, solar: %f kW", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time), available_solar_power); + } + else if(available_solar_power < geyser_power) { + id(geyser_relay_status) = false; + ESP_LOGI(tag, "Economy mode: not enough solar energy, geyser turned to be off at %f °C. Heating start: %s, end: %s, time: %d, solar: %f kW", top_temp, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time), available_solar_power); + } + } + else { + // GEYSER IS NOT ENERGISED + // ======================= + if(sun_high_enough) { + id(geyser_relay_status) = true; + } + } + } + + # calculates effective geyser temp, taking into account both bottom and top geyser temperatures + - id: calc_geyser_heating_values + parameters: + temperature_target: double + then: + - lambda: |- + double top_temp = id(geyser_top_temperature).state; + // estimate expected heat loss + double heat_loss = id(thermal_transmittance) * id(geyser_surface_area) * (top_temp - id(ambient_temperature).state); // in Watts + double heating_power = id(geyser_element_power).state - heat_loss; + id(g_heat_loss) = heat_loss; + id(geyser_effective_power) = (heating_power > 0.0001) ? heating_power : 0.0001; // this is to avoid dividing by zero + // use specific_heat_capacity = 4184 J/kg°C to calculate heating factor, i.e. the number of seconds it will take to heat water by 1 degree + double heating_factor = id(watermass) * 4184 / id(geyser_effective_power); + // set estimated heat required + double geyser_temp_diff = top_temp - id(geyser_bottom_temperature).state - id(geyser_top_bottom_constraint); + double geyser_effective_temperature = (geyser_temp_diff > 0) ? top_temp - geyser_temp_diff : top_temp; + double temperature_diff = temperature_target - geyser_effective_temperature; + // set estimated heating time + double heating_time = heating_factor * temperature_diff; // in Joules + id(estimated_heating_time) = static_cast(heating_time); + double overshoot_period = heating_factor * id(temp_overshoot_allowed); + id(estimated_heating_overshoot_time) = static_cast(overshoot_period + heating_time); + + - id: record_heat_gained + then: + - lambda: |- + const char tag[] = "geyser"; + //ESP_LOGI(tag, "Recording heat lost/gained."); + auto currenttime = id(time_source).now(); + id(heat_monitor_end) = currenttime.timestamp; + time_t start_time = id(heat_monitor_start); + time_t time_elapsed = id(heat_monitor_end) - start_time; + double top_temp = id(geyser_top_temperature).state; + if(time_elapsed > 0) { + // heat gained measurement + if(start_time > 0) { + double water_temp = top_temp; + double ambient_temp = id(ambient_temperature).state; + double previous_temp = id(last_geyser_top_temperature); + if(isnan(water_temp)) { + ESP_LOGW("warning", "Geyser top temperature is NaN. Skipping heat gain measurement."); + } + else if (previous_temp < -280.0) { + ESP_LOGW("warning", "Geyser previous top temperature (%.2f) is invalid. Restarting heat gain measurement.", previous_temp); + id(start_heat_monitor).execute(water_temp, ambient_temp); + } + else { + double dtemp = water_temp - previous_temp; + double heat_energy_gained = id(watermass) * 4184 * dtemp; // joules + double heat_gain = heat_energy_gained / time_elapsed; // watts + id(g_heat_gained) = heat_gain; + ESP_LOGI(tag, "Geyser temperature loss/gain: %.2f°C, time elapsed %d, heat energy gained: %.0fJ, heat gain: %.0fW", dtemp, time_elapsed, heat_energy_gained, heat_gain); + id(start_heat_monitor).execute(water_temp, ambient_temp); + } + } + } + + - id: start_heat_monitor + parameters: + water_temp: double + ambient_temp: double + then: + - lambda: |- + double top_temp = id(geyser_top_temperature).state; + //ESP_LOGI(tag, "Starting heat loss/gain measurement. A: %.2f, T: %.2f", ambient_temp, water_temp); + auto currenttime = id(time_source).now(); + id(heat_monitor_start) = currenttime.timestamp; + if(isnan(water_temp)) { + ESP_LOGW("warning", "Geyser top temperature is NaN. Setting last_geyser_top_temperature to default."); + id(last_geyser_top_temperature) = -301; + } + else { + id(last_geyser_top_temperature) = water_temp; + if(isnan(ambient_temp)) { + ESP_LOGW("warning", "Ambient temperature is NaN. Setting last_temp_diff to default."); + id(last_temp_diff) = -301; + } + else { + id(last_temp_diff) = water_temp - ambient_temp; + } + //ESP_LOGI(tag, "Start monitor @ Geyser top temperature: %.2f°C, geyser vs outside: %.2f°C", id(last_geyser_top_temperature), id(last_temp_diff)); + } + + - id: init_fixed_public_holidays + then: + - lambda: |- + id(fixed_public_holidays)[0][0] = 1; // New Year's Day + id(fixed_public_holidays)[0][1] = 1; + id(fixed_public_holidays)[1][0] = 3; // Human Rights Day + id(fixed_public_holidays)[1][1] = 21; + id(fixed_public_holidays)[2][0] = 4; // Freedom Day + id(fixed_public_holidays)[2][1] = 27; + id(fixed_public_holidays)[3][0] = 5; // Workers Day + id(fixed_public_holidays)[3][1] = 1; + id(fixed_public_holidays)[4][0] = 6; // Youth Day + id(fixed_public_holidays)[4][1] = 16; + id(fixed_public_holidays)[5][0] = 8; // Womens Day + id(fixed_public_holidays)[5][1] = 9; + id(fixed_public_holidays)[6][0] = 9; // Heritage Day + id(fixed_public_holidays)[6][1] = 24; + id(fixed_public_holidays)[7][0] = 12; // Reconciliation Day + id(fixed_public_holidays)[7][1] = 16; + id(fixed_public_holidays)[8][0] = 12; // Christmas Day + id(fixed_public_holidays)[8][1] = 25; + id(fixed_public_holidays)[9][0] = 12; // Boxing Day + id(fixed_public_holidays)[9][1] = 26; + + - id: init_schedule + then: + - lambda: |- + // SUNDAYS + int i = ${GM_SUNDAY}; + int j = 0; + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 0, 7); // IDLE + id(set_schedule_block).execute(i, j++, ${HEATING_WARM}, 7, 8); // EARLY MORNING + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 8, 9); // MORNING + id(set_schedule_block).execute(i, j++, ${HEATING_LUKE_WARM}, 9, ${LATE_MORNING_END}); // LATE MORNING + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, ${LATE_MORNING_END}, 16); // MAIN HEAT (THERMOSTAT CONTROL) + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 28.5); // IDLE (28.5 = 4:30AM next day) + // WEEKDAYS + i = ${GM_WORKDAY}; + j = 0; + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 0, 4.5); + id(set_schedule_block).execute(i, j++, ${HEATING_WARM}, 4.5, 6); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 6, 9); + id(set_schedule_block).execute(i, j++, ${HEATING_LUKE_WARM}, 9, ${LATE_MORNING_END}); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, ${LATE_MORNING_END}, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 28.5); + // SATURDAYS + i = ${GM_SATURDAY}; + j = 0; + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 0, 7); + id(set_schedule_block).execute(i, j++, ${HEATING_WARM}, 7, 8); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 8, 9); + id(set_schedule_block).execute(i, j++, ${HEATING_LUKE_WARM}, 9, ${LATE_MORNING_END}); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, ${LATE_MORNING_END}, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 31); // (31 = 7AM next day) + // PUBLIC HOLIDAYS + i = ${GM_PUBLIC_HOLIDAY}; + j = 0; + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 0, 7); + id(set_schedule_block).execute(i, j++, ${HEATING_WARM}, 7, 8); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 8, 9); + id(set_schedule_block).execute(i, j++, ${HEATING_LUKE_WARM}, 9, ${LATE_MORNING_END}); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, ${LATE_MORNING_END}, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 31); + // SCHOOL HOLIDAYS + i = ${GM_SCHOOL_HOLIDAY}; + j = 0; + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 0, 7); + id(set_schedule_block).execute(i, j++, ${HEATING_WARM}, 7, 8); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 8, 9); + id(set_schedule_block).execute(i, j++, ${HEATING_LUKE_WARM}, 9, ${LATE_MORNING_END}); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, ${LATE_MORNING_END}, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 31); + + - id: set_schedule_block + parameters: + day_idx: uint + block_idx: uint + temperature: float + start_time: float + end_time: float + then: + - lambda: |- + if(day_idx < 0 || day_idx > ${GEYSER_MODES}) { + ESP_LOGW("Set Schedule", "day index of %d is out of bounds. Allowed values: 0 to %d.", day_idx, ${GEYSER_MODES}); + return; + } + if(block_idx < 0 || block_idx > ${HEATING_DAY_BLOCKS}) { + ESP_LOGW("Set Schedule", "block index of %d is out of bounds. Allowed values: 0 to %d.", block_idx, ${HEATING_DAY_BLOCKS}); + return; + } + id(g_schedule)[day_idx][block_idx][0] = static_cast(temperature * ${HEATING_TEMP_SCALE}); + id(g_schedule)[day_idx][block_idx][1] = static_cast(start_time * 3600); + id(g_schedule)[day_idx][block_idx][2] = static_cast(end_time * 3600); + // ESP_LOGI("SCHEDULE", "// %.2f, %d, (%.2f), %d (%.2f)", temperature, id(g_schedule)[day_idx][block_idx][1], start_time, id(g_schedule)[day_idx][block_idx][2], end_time); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][0] = %d;", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][0]); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][1] = %d;", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][1]); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][2] = %d;\n", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][2]); + + - id: show_schedule + then: + - lambda: |- + for(int d = 0; d < ${GEYSER_MODES}; d++) { + for(int b = 0; b < ${HEATING_DAY_BLOCKS}; b++) { + int t = id(g_schedule)[d][b][0]; + int s = id(g_schedule)[d][b][1]; + int e = id(g_schedule)[d][b][2]; + float temp = static_cast(t) / ${HEATING_TEMP_SCALE}; + float start_time = static_cast(s) / 3600; + float end_time = static_cast(e) / 3600; + ESP_LOGI("SCHEDULE", "// %.1f°C, %d, (%.2f), %d (%.2f)", temp, s, start_time, e, end_time); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][0] = %d;", d, b, t); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][1] = %d;", d, b, s); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][2] = %d;\n", d, b, e); + } + } + + # here we add Easter to public holidays + - id: init_holidays + then: + - lambda: |- + // calculate easter first + #include + auto today = id(time_source).now(); + today.second = 0; + today.minute = 0; + today.hour = 0; + auto year = today.year; + auto datevalue = fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)+fmod(32+2*fmod(trunc(year/100),4)+2*trunc(fmod(year,100)/4)-fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)-fmod(year,4),7)-7*trunc((fmod(year,19)+11*fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)+22*fmod(32+2*fmod(trunc(year/100),4)+2*trunc(fmod(year,100)/4)-fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)-fmod(year,4),7))/451)+114; + today.month = trunc(datevalue/31); + today.day_of_month = 1+fmod(datevalue,31); + today.recalc_timestamp_local(); + auto time_obj = ESPTime::from_epoch_local(today.timestamp - 2*86400); // Good Friday + int i = 0; + id(public_holidays)[i][0] = time_obj.month; + id(public_holidays)[i][1] = time_obj.day_of_month; + //ESP_LOGI(tag, "======== Set holiday h_idx:%d, %d-%d-%d [%d]", i, time_obj.year, time_obj.month, time_obj.day_of_month, time_obj.day_of_week); + time_obj = ESPTime::from_epoch_local(today.timestamp + 86400); // Easter Monday + i++; + id(public_holidays)[i][0] = time_obj.month; + id(public_holidays)[i][1] = time_obj.day_of_month; + //ESP_LOGI(tag, "======== Set holiday h_idx:%d, %d-%d-%d [%d]", i, time_obj.year, time_obj.month, time_obj.day_of_month, time_obj.day_of_week); + // do rest of public holidays + int j = 0; // fixed_public_holidays array index + while(j < 10) { + ++i; + time_obj.year = year; + time_obj.month = id(fixed_public_holidays)[j][0]; + time_obj.day_of_month = id(fixed_public_holidays)[j][1]; + time_obj.recalc_timestamp_local(); + auto holiday = ESPTime::from_epoch_local(time_obj.timestamp); // we need a new struct as the time_obj does not update day_of_week from here onwards (don't know why) + bool isBoxingDay = (holiday.month == 12) && (holiday.day_of_month == 26); + if(holiday.day_of_week == 1) { // if Sunday + holiday.increment_day(); // then Monday is also public holiday + holiday.recalc_timestamp_local(); + //ESP_LOGI(tag, "======== Monday is also public holiday if public holiday falls on a Sunday. h_idx:%d, fh_idx:%d, %d-%d-%d [%d]", i, j, holiday.year, holiday.month, holiday.day_of_month, holiday.day_of_week); + } + else { + if(isBoxingDay && holiday.day_of_week == 2) { + holiday.increment_day(); // then if President so decides, Tuesday is usually also public holiday + holiday.recalc_timestamp_local(); + //ESP_LOGI(tag, "======== Boxing Day falls on a Monday so Tuesday is also public holiday. h_idx:%d, fh_idx:%d, %d-%d-%d [%d]", i, j, holiday.year, holiday.month, holiday.day_of_month, holiday.day_of_week); + } + } + holiday.recalc_timestamp_local(); + id(public_holidays)[i][0] = holiday.month; + id(public_holidays)[i][1] = holiday.day_of_month; + holiday.recalc_timestamp_local(); + //ESP_LOGI(tag, "======== Set holiday h_idx:%d, fh_idx:%d, %d-%d-%d [%d]", i, j, holiday.year, holiday.month, holiday.day_of_month, holiday.day_of_week); + j++; + } + + - id: canbus_add_to_queue + parameters: + can_id_set: std::set& + max_requests: int + then: + lambda: |- + if(!can_id_set.empty()) { + // check how many times can_ids are queued already + auto qcpy = id(g_cb_request_queue); + std::map request_counts; + while (!qcpy.empty()) { + auto& cid_set = qcpy.front(); + for(auto& can_id : can_id_set) { + if(cid_set.contains(can_id)) { + const auto& ret = request_counts.emplace(can_id, 1); + if(!ret.second) { + auto& kvp = *ret.first; + kvp.second++; + } + } + } + qcpy.pop(); + } + std::set newset; + // re-insert only those can-ids into newset that are queued less than max_requests times + for(const auto& kvp : request_counts) { + const auto& count = kvp.second; + if(count <= max_requests) { + newset.insert(kvp.first); + } + //else { + // ESP_LOGI("request_counts", "CAN_ID 0x%X, COUNT: %d not inserted!", kvp.first, count); + //} + } + // insert newset at the back of the queue with can_id request counts < max_requests + id(g_cb_request_queue).push(newset); + } + - id: send_battery_info_request + then: + lambda: |- + auto time_obj = id(time_source).now(); + auto elapsedtime = time_obj.timestamp - id(last_battery_message_time); + if(elapsedtime >= ${BATTERY_INFO_TIMEOUT}) { + // for now we just requesting BATTERY_STATE + ESP_LOGI("battery info timeout", "sending request for CAN_ID 0x%X", solar::cbf_pylon::CB_BATTERY_STATE); + id(g_cb_cache).send_request(id(canbus_solarbattery), solar::cbf_pylon::CB_BATTERY_STATE); + } + + - id: canbus_send_heartbeat + then: + lambda: |- + using namespace solar; + std::vector x(cbf_sthome::heartbeat.begin(), cbf_sthome::heartbeat.end()); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_CANBUS_ID08, x); + + # this one has a success output parameter, which will determine whether the frame should be resent later + - id: canbus_send_temperature_top + parameters: + success: bool& + then: + lambda: |- + using namespace solar; + auto temperature = id(geyser_top_temperature).raw_state; + success = !isnan(temperature); + if(success) { + auto x = cb_frame::get_byte_stream(temperature, -256); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_GEYSER_TEMPERATURE_TOP, x); + } + + # this one has a success output parameter, which will determine whether the frame should be resent later + - id: canbus_send_temperature_bottom + parameters: + success: bool& + then: + lambda: |- + using namespace solar; + auto temperature = id(geyser_bottom_temperature).raw_state; + success = !isnan(temperature); + if(success) { + auto x = cb_frame::get_byte_stream(temperature, -256); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_GEYSER_TEMPERATURE_BOTTOM, x); + } + + # this one has a success output parameter, which will determine whether the frame should be resent later + - id: canbus_send_temperature_ambient + parameters: + success: bool& + then: + lambda: |- + using namespace solar; + auto temperature = id(ambient_temperature).raw_state; + success = !isnan(temperature); + if(success) { + auto x = cb_frame::get_byte_stream(temperature, -256); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_GEYSER_TEMPERATURE_AMBIENT, x); + } + + - id: canbus_send_geyser_heating + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(heating_loss).state, -64, id(heat_gained).state, 128, id(calculated_heat_loss).state, 128, id(estimated_heating_time), 1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_GEYSER_HEATING, x); + + - id: canbus_send_geyser_active_schedule + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(active_schedule_temp).state, -256, id(active_heating_time), -1, id(estimated_heating_overshoot_time), -64, id(active_schedule_day).state, 1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_GEYSER_ACTIVE_SCHEDULE, x); + + - id: canbus_send_power_mains + then: + lambda: |- + using namespace solar; + auto x = 0; //cb_frame::get_byte_stream(id(mains_power).state, 2048, id(mains_voltage_adc).state, 128, id(mains_current).state, 512); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_MAINS, x); + - id: canbus_send_power_inverter + then: + lambda: |- + using namespace solar; + auto x = 0;// cb_frame::get_byte_stream(id(total_inverter_output).state, 2048, id(inverter_output_voltage_adc).state, 128, id(inverter1_2_output_current).state, 512); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_INVERTER, x); + + - id: canbus_send_power_plugs + then: + lambda: |- + using namespace solar; + auto x = 0;// cb_frame::get_byte_stream(id(power_outlets_power).state, 2048, id(inverter_output_voltage_adc).state, 128, id(power_outlets_current).state, 512); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_PLUGS, x); + - id: canbus_send_power_lights + then: + lambda: |- + using namespace solar; + auto x = 0;//cb_frame::get_byte_stream(id(lights_power).state, 2048, id(inverter_output_voltage_adc).state, 128, id(lights_current).state, 512); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_LIGHTS, x); + - id: canbus_send_power_geyser + then: + lambda: |- + using namespace solar; + auto x = 0;//cb_frame::get_byte_stream(id(geyser_power).state, 2048, id(inverter_output_voltage_adc).state, 128, id(geyser_current).state, 512); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_GEYSER, x); +# - id: canbus_send_power_pool +# then: +# lambda: |- +# using namespace solar; +# auto x = cb_frame::get_byte_stream(id(pool_power).state, 2048, id(inverter_output_voltage_adc).state, 128, id(pool_current).state, 512); +# id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_POOL, x); + - id: canbus_send_power_generated + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(generated_power).state, 2048, id(power_loss).state, 2048); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_POWER_GENERATED, x); + + + - id: canbus_send_controller_states + then: + lambda: |- + using namespace solar; + std::vector byte_stream(3, 0); + uint8_t& alarms = byte_stream[0]; + uint8_t& states = byte_stream[1]; + uint8_t& modes = byte_stream[2]; + int geysermode = 0; + id(get_geyser_mode).execute(geysermode); + alarms = ((id(inverter_battery_charge_state).state) ? 0x80 : 0) | ((id(inverter1_2_overload).state) ? 0x08 : 0); + states = ((id(geyser_heating).state) ? 0x80 : 0) | ((id(geyser_relay).state) ? 0x40 : 0) | ((id(mains_supply).state) ? 0x20 : 0) | ((id(battery_charging).state) ? 0x08 : 0); + modes = ((id(economy_mode).state) ? 0x80 : 0) | ((geysermode << 4) & 0x70); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_CONTROLLER_STATES, byte_stream); + + - id: canbus_send_energy_mains + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_mains_energy).state, 512, id(monthly_mains_energy).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_mains_energy).state, 2, id(mains_energy).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_MAINS, x); + + - id: canbus_send_energy_geyser + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_geyser_energy).state, 512, id(monthly_geyser_energy).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_geyser_energy).state, 2, id(geyser_energy).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_GEYSER, x); + +# - id: canbus_send_energy_pool +# then: +# lambda: |- +# using namespace solar; +# auto x = cb_frame::get_byte_stream(id(daily_pool_energy).state, 512, id(monthly_pool_energy).state, 32, id(yearly_pool_energy).state, 2, id(pool_energy).state, 0.1); +# id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_POOL, x); + + - id: canbus_send_energy_plugs + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_plugs_energy).state, 512, id(monthly_plugs_energy).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_plugs_energy).state, 2, id(plugs_energy).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_PLUGS, x); + + - id: canbus_send_energy_lights + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_lights_energy).state, 512, id(monthly_lights_energy).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_lights_energy).state, 2, id(lights_energy).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_LIGHTS, x); + + - id: canbus_send_energy_house + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_house_energy_usage).state, 512, id(monthly_house_energy_usage).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_house_energy_usage).state, 2, id(house_energy_usage).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_HOUSE, x); + + - id: canbus_send_energy_generated + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_generated_energy).state, 512, id(monthly_generated_energy).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_generated_energy).state, 2, id(generated_energy).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_GENERATED, x); + + - id: canbus_send_energy_loss + then: + lambda: |- + using namespace solar; + auto x = cb_frame::get_byte_stream(id(daily_energy_loss).state, 512, id(monthly_energy_loss).state, 32, 0.0, 2, 0.0, 0.1); // id(yearly_energy_loss).state, 2, id(energy_loss).state, 0.1); + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_sthome::CB_ENERGY_LOSS, x); + + - id: canbus_send_battery_limits + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_LIMITS); + + - id: canbus_send_battery_state + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_STATE); + + - id: canbus_send_battery_status + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_STATUS); + + - id: canbus_send_battery_fault + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_FAULT); + + - id: canbus_send_battery_request_flags + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_REQUEST_FLAGS); + + - id: canbus_send_battery_manufacturer + then: + lambda: |- + using namespace solar; + id(g_cb_cache).send_frame(id(canbus_sthome), cbf_pylon::CB_BATTERY_MANUFACTURER); + +#################################################################################################################################### +#################################################################################################################################### +# Geyser HEATING Calculations +# HEAT LOSS +# The primary formula for calculating heat loss in a water heater is Q = U x A x ΔT, where: +# Q: is the heat loss (in Watts, BTU/hr, etc.) +# U: is the U-value (thermal transmittance) of the heater's insulation (in W/m²°C or BTU/hr ft²°F). +# A: is the surface area of the water heater (in m² or ft²). +# ΔT: is the temperature difference between the water inside the heater and the ambient temperature outside (in °C or °F). +# +# HEAT REQUIRED +# Heat Required in Joules (Q) = m * ΔT * c where: +# m = mass of water in kg +# ΔT = temperature difference in °C +# c = specific heat capacity (water = 4184 J/kg°C). +# +# HEATING TIME +# Heating Time in seconds (t) = Q / W where: +# Q = heat required in Joules +# W = power in Watts of heating element +# +############################################################################################################### +# Alternative ADS1115 sensor +#sensor: + #ads1115_48 +# Sensor will convert ADC output to Current without need for ct_clamp platform sensor +# - platform: ads1115 +# multiplexer: 'A0_A1' +# gain: 1.024 +# name: "Geyser Element Current" +# ads1115_id: ads1115_48 +# update_interval: 0ms #24ms +# id: geyser_element_current_real +# state_class: measurement +# device_class: current +# unit_of_measurement: "A" +# icon: "mdi:flash" +# accuracy_decimals: 8 +# filters: +# # Calculates RMS voltage sampled by the ADS1115 ADC +# - lambda: return x * x; #### +# - sliding_window_moving_average: # +# window_size: 2500 # averages over 2500 update intervals +# send_every: 1250 # reports every 1250 update intervals +# - lambda: return sqrt(x); #### +# - multiply: 88.2 # Map measured voltage from CT clamp to current in the primary circuit + +# CT CLAMP calculations +# Burden Resistor (ohms) = (VREF * CT TURNS) / (√2 * max primary current) +# Primary Current (A) = secondary voltage * CT TURNS / (√2 * burden resistor) +# CT TURNS = primary current * burden resistor / secondary voltage +# Multiplier = CT TURNS / burden resistor (other surrounding circuitry impacts this value) + +# for use with latching relay +# - platform: template +# id: geyser_relay_failures +# name: "Geyser Relay Failures" +# icon: mdi:flash +# accuracy_decimals: 0 +# unit_of_measurement: "" +# lambda: |- +# auto failcount = id(geyser_relay_fail_count); +# if(failcount > 0) { +# id(geyser_relay_fail).turn_on(); +# } +# else { +# id(geyser_relay_fail).turn_off(); +# } +# return failcount; +# update_interval: 10s +# + +# for Benchwork +# - platform: template +# name: "Geyser Top Temperature" +# id: geyser_top_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 60.5114; +# +# - platform: template +# name: "Geyser Bottom Temperature" +# id: geyser_bottom_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 31.2455; +# +# - platform: template +# name: "Ambient Temperature" +# id: ambient_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 20.1234; +# end of for Benchwork +# +# script: +# - id: update_power_counters +# then: +# - lambda: |- +# if(id(time_synched)) { +# // power counters +# auto time_obj = id(time_source).now(); +# time_obj.recalc_timestamp_local(); +# int day_of_week = time_obj.day_of_week; +# time_t end_time = static_cast(time_obj.timestamp); +# time_t start_time = static_cast(id(timer_start)); +# double time_elapsed = static_cast(end_time - start_time); +# id(validate_energy_values).execute(day_of_week); +# if(start_time > 0) { +# id(do_power_counters_update).execute(day_of_week, time_elapsed); +# } +# id(timer_start) = end_time; +# } +# +# - id: init_daily_power_counters +# then: +# - lambda: |- +# auto currenttime = id(time_source).now(); +# int day_of_week = currenttime.day_of_week; +# id(geyser_energy_daily)[day_of_week-1] = 0.0; // reset +# id(power_outlets_energy_daily)[day_of_week-1] = 0.0; // reset +# id(mains_energy_daily)[day_of_week-1] = 0.0; // reset +# id(generated_energy_daily)[day_of_week-1] = 0.0; // reset +# id(energy_loss_daily)[day_of_week-1] = 0.0; // reset +# +# - id: init_monthly_power_counters +# then: +# - lambda: |- +# //auto currenttime = id(time_source).now(); +# //int day_of_week = currenttime.day_of_week; +# id(geyser_energy) = 0.0; // reset +# id(power_outlets_energy) = 0.0; // reset +# id(mains_energy) = 0.0; // reset +# id(generated_energy) = 0.0; // reset +# id(energy_loss) = 0.0; // reset +# +# - id: do_power_counters_update +# parameters: +# day_of_week: int +# time_elapsed: double +# then: +# - lambda: |- +# double power = id(geyser_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Geyser power is NaN. Skipping geyser power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(geyser_energy_daily)[day_of_week-1] += energy; +# id(geyser_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(power_outlets_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Plugs Power is NaN. Skipping Plugs Power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(power_outlets_energy_daily)[day_of_week-1] += energy; +# id(power_outlets_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(mains_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Mains power is NaN. Skipping mains power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(mains_energy_daily)[day_of_week-1] += energy; +# id(mains_energy) += energy; +# } +# power = id(lights_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Lights power is NaN. Skipping lights power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(lights_energy_daily)[day_of_week-1] += energy; +# id(lights_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(generated_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Generated power is NaN. Skipping generated power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(generated_energy_daily)[day_of_week-1] += energy; +# id(generated_energy) += energy; +# //ESP_LOGI(tag, "Generated energy: %f kWs. Total: %f kWh", energy, id(generated_energy)/3600.0); +# } +# power = id(power_loss).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Energy loss is NaN. Skipping energy loss counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(energy_loss_daily)[day_of_week-1] += energy; +# id(energy_loss) += energy; +# } +# - id: validate_energy_values +# parameters: +# day_of_week: int +# then: +# - lambda: |- +# if(isnan(id(geyser_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Geyser Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(geyser_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(power_outlets_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Plugs Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(power_outlets_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(mains_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Mains Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(mains_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(lights_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Lights Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(lights_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(generated_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Generated Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(generated_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(geyser_energy))) { +# ESP_LOGW("warning", "Geyser Energy is NaN. Value was reset to zero."); +# id(geyser_energy) = 0; +# } +# if(isnan(id(power_outlets_energy))) { +# ESP_LOGW("warning", "Plugs Energy is NaN. Value was reset to zero."); +# id(power_outlets_energy) = 0; +# } +# if(isnan(id(mains_energy))) { +# ESP_LOGW("warning", "Mains Energy is NaN. Value was reset to zero."); +# id(mains_energy) = 0; +# } +# if(isnan(id(lights_energy))) { +# ESP_LOGW("warning", "Lights Energy is NaN. Value was reset to zero."); +# id(lights_energy) = 0; +# } +# if(isnan(id(generated_energy))) { +# ESP_LOGW("warning", "Generated Energy is NaN. Value was reset to zero."); +# id(generated_energy) = 0; +# } \ No newline at end of file