diff --git a/components/ads1115_int/ads1115_int.cpp b/components/ads1115_int/ads1115_int.cpp index cb235c6..58dcd78 100644 --- a/components/ads1115_int/ads1115_int.cpp +++ b/components/ads1115_int/ads1115_int.cpp @@ -14,14 +14,14 @@ static const uint8_t ADS1115_REGISTER_HI_TRESH = 0x03; void ADS1115Component::setup() { uint16_t value; + if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { + this->mark_failed("Could not read ADS1115 conversion register on setup."); + return; + } // 1. Create a binary semaphore data_ready_sem = xSemaphoreCreateBinary(); this->alert_pin_->setup(); this->alert_pin_->attach_interrupt(&ADS1115Component::isr_handler, this, gpio::INTERRUPT_FALLING_EDGE); - if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { - this->mark_failed(); - return; - } // setup with default values uint16_t config = 0; // Clear single-shot bit @@ -60,12 +60,13 @@ void ADS1115Component::setup() config |= 0b0000000000000011; if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { - this->mark_failed(); + this->mark_failed("Could not write to ADS1115 config register on setup."); return; } + this->prev_config_ = config; if(!this->set_data_ready_mode()) { - this->mark_failed(); + this->mark_failed("Could not set ADS1115 data ready mode on setup."); return; } } @@ -92,23 +93,42 @@ void ADS1115Component::loop() // we read data for the current mux index, then set up the next conversion void ADS1115Component::read_request_next() { - int mux_index = this->mux_data_index_ % 4; - auto multiplexer = this->muxdata_[mux_index].multiplexer; - if(multiplexer == ADS1115_MULTIPLEXER_INVALID) { - ESP_LOGE(TAG, "MUX data not set for index %d", mux_index); - } - else { - int16_t raw_value = 0; - double v = this->read_data(raw_value, mux_index); - if (!std::isnan(v)) { - auto sensor_ptr = this->muxdata_[mux_index].sensor_ptr; - ESP_LOGV(TAG, "'% -18s': Raw=% 6d %fV", sensor_ptr->get_name().c_str(), raw_value, v); - sensor_ptr->publish_state(v); + uint16_t config = 0x0000; + // We do not support multiple ads1115 sharing the same alert/rdy pin, however we still read the ads1115 config register to get the multiplexer value. This allows us to also check the operational status (OS) bit. + // If OS bit set, i.e. the conversion is done meaning we can read the data; this is valid only in single shot mode, in continuous mode (which is not supported by this code) we can read data without checking the status bit + //ESP_LOGD(TAG, "Config: 0x%04X", this->prev_config_); + if(this->read_byte_16(ADS1115_REGISTER_CONFIG, &config)) { + //ESP_LOGD(TAG, "Config register: 0x%04X", config); + int ads_mux = (config >> 12) & 0b111; + bool conversion_done = (config >> 15) == 1; + //ESP_LOGD(TAG, "Current MUX setting according to ADS1115: %d", ads_mux); + int ads_mux_index = get_mux_index(static_cast(ads_mux)); + //ESP_LOGD(TAG, "Current MUX index according to ADS1115: %d", ads_mux_index); + if (conversion_done && ads_mux_index != -1) { + int16_t raw_value = 0; + double v = this->read_data(raw_value, ads_mux_index); + if (!std::isnan(v)) { + auto sensor_ptr = this->muxdata_[ads_mux_index].sensor_ptr; + ESP_LOGV(TAG, "'% -18s': Raw=% 6d %fV", sensor_ptr->get_name().c_str(), raw_value, v); + sensor_ptr->publish_state(v); + } + // we have read data for the current mux index, move to next + int next_index = next_mux_data_index(ads_mux_index); + if(next_index != -1) { + this->start_single_shot_conversion(next_index); + } } } - if(next_mux_data_index() != -1) { - this->start_single_shot_conversion(); +} + +int ADS1115Component::get_mux_index(ADS1115Multiplexer multiplexer) +{ + for(int i=0; i<4; i++) { + if(this->muxdata_[i].multiplexer == multiplexer) { + return i; + } } + return -1; // not found } double ADS1115Component::read_data(int16_t& raw_value, int mux_index) @@ -192,28 +212,34 @@ int ADS1115Component::set_mux_data(ADS1115Multiplexer multiplexer, ADS1115Gain g bool ADS1115Component::set_data_ready_mode() { + bool success = true; uint16_t config = this->prev_config_; // Set comparator que mode - assert after one conversion // 0bxxxxxxxxxxxxxx00 config &= 0b1111111111111100; if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { - return false; + ESP_LOGE(TAG, "Failed to write to ads1115 to set comparator que mode"); + success = false; } - this->prev_config_ = config; - if(!this->write_byte_16(ADS1115_REGISTER_HI_TRESH, 0x8000)) { - return false; + else { + this->prev_config_ = config; + if(!this->write_byte_16(ADS1115_REGISTER_HI_TRESH, 0x8000)) { + ESP_LOGE(TAG, "Failed to write to ads1115 to set high threshold register for alert pin"); + success = false; + } + else { + if(!this->write_byte_16(ADS1115_REGISTER_LO_TRESH, 0x0000)) { + ESP_LOGE(TAG, "Failed to write to ads1115 to set low threshold register for alert pin"); + success = false; + } + } } - if(!this->write_byte_16(ADS1115_REGISTER_LO_TRESH, 0x0000)) { - return false; - } - return true; + return success; } - -bool ADS1115Component::start_single_shot_conversion() +bool ADS1115Component::start_single_shot_conversion(int mux_index) { - int mux_index = this->mux_data_index_; auto& muxdata = this->muxdata_[mux_index]; auto multiplexer = muxdata.multiplexer; if (multiplexer != ADS1115_MULTIPLEXER_INVALID) { @@ -248,15 +274,16 @@ bool ADS1115Component::start_single_shot_conversion() } return false; } -int ADS1115Component::next_mux_data_index() + +int ADS1115Component::next_mux_data_index(int start_index) { - int start_index = this->mux_data_index_; + int result = start_index; do { - this->mux_data_index_ = (this->mux_data_index_ + 1) % 4; - if(this->muxdata_[this->mux_data_index_].multiplexer != ADS1115_MULTIPLEXER_INVALID) { - return this->mux_data_index_; + result = (result + 1) % 4; + if(this->muxdata_[result].multiplexer != ADS1115_MULTIPLEXER_INVALID) { + return result; } - } while(this->mux_data_index_ != start_index); + } while(result != start_index); return -1; // no valid mux data found } diff --git a/components/ads1115_int/ads1115_int.h b/components/ads1115_int/ads1115_int.h index edac85f..9ee1ebc 100644 --- a/components/ads1115_int/ads1115_int.h +++ b/components/ads1115_int/ads1115_int.h @@ -60,10 +60,11 @@ class ADS1115Component : public Component, public i2c::I2CDevice { sensor::Sensor *sensor_ptr; mux_data_item() { multiplexer = ADS1115_MULTIPLEXER_INVALID; } }; - int next_mux_data_index(); - void read_request_next(); int mux_data_index_{0}; mux_data_item muxdata_[4]; + int next_mux_data_index(int start_index); + void read_request_next(); + int get_mux_index(ADS1115Multiplexer multiplexer); public: // Semaphore handle @@ -72,7 +73,7 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void loop() override; void dump_config() override; double read_data(int16_t& raw_value, int mux_index); - bool start_single_shot_conversion(); + bool start_single_shot_conversion(int mux_index); void set_alert_pin(InternalGPIOPin *pin) { alert_pin_ = pin; } bool set_data_ready_mode(); int set_mux_data(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, ADS1115Samplerate samplerate, sensor::Sensor *sensor_ptr); diff --git a/components/ads1115_int/sensor/ads1115_int_sensor.cpp b/components/ads1115_int/sensor/ads1115_int_sensor.cpp index 8a3cbaf..0fdcdcb 100644 --- a/components/ads1115_int/sensor/ads1115_int_sensor.cpp +++ b/components/ads1115_int/sensor/ads1115_int_sensor.cpp @@ -10,9 +10,9 @@ static const char *const TAG = "ads1115_int.sensor"; void ADS1115Sensor::loop() { if(this->first_reading_) { - this->parent_->set_mux_data(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_, this); + int mux_index = this->parent_->set_mux_data(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_, this); this->first_reading_ = false; - this->parent_->start_single_shot_conversion(); // this will set off the first conversion; subsequent conversions will be triggered by the ISR + this->parent_->start_single_shot_conversion(mux_index); // this will set off the first conversion; subsequent conversions will be triggered by the ISR } } diff --git a/components/ads1115_pol/ads1115_pol.cpp b/components/ads1115_pol/ads1115_pol.cpp index 35abc0a..f099e29 100644 --- a/components/ads1115_pol/ads1115_pol.cpp +++ b/components/ads1115_pol/ads1115_pol.cpp @@ -12,7 +12,7 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; void ADS1115Component::setup() { uint16_t value; if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { - this->mark_failed(); + this->mark_failed(/*LOG_STR*/("Could not read ADS1115 conversion register on setup.")); return; } @@ -59,13 +59,13 @@ void ADS1115Component::setup() { config |= 0b0000000000000011; if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { - this->mark_failed(); + this->mark_failed(/*LOG_STR*/("Could not write to ADS1115 config register on setup.")); return; } this->prev_config_ = config; } void ADS1115Component::dump_config() { - ESP_LOGCONFIG(TAG, "ADS1115:"); + ESP_LOGCONFIG(TAG, "ADS1115_pol:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/sthome-ut8.yaml b/sthome-ut8.yaml index f036a67..e6c0769 100644 --- a/sthome-ut8.yaml +++ b/sthome-ut8.yaml @@ -1,3 +1,4 @@ +# SIMPLIFIED CONFIGURATION FILE FOR sthome-ut8 DEVICE external_components: - source: type: local @@ -6,200 +7,22 @@ external_components: 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: - - priority: 600 # This is where most sensors are set up (higher number means higher priority) - then: - - ds1307.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] + then: + - lambda: |- + ESP_LOGI("esphome", "Device booted"); -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 @@ -221,10 +44,13 @@ logger: light: INFO sensor: INFO ds1307: INFO + tlc59208f: DEBUG text_sensor: INFO ads1115.sensor: INFO + ads1115_pol: DEBUG ads1115_pol.sensor: INFO - ads1115_int.sensor: DEBUG + ads1115_int: DEBUG + ads1115_int.sensor: INFO modbus: INFO modbus_controller: INFO modbus_controller.sensor: INFO @@ -250,11 +76,70 @@ 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: +# address: 0x20 +# id: tlc59208f_1 +# i2c_id: bus_a +# +#output: +# - platform: ledc +# pin: +# number: GPIO12 #GPIO26 # LED_LOW_BAT +# inverted: false #true +# id: led_inverter_battery_low +# +# - platform: tlc59208f +# channel: 0 +# tlc59208f_id: 'tlc59208f_1' +# id: led0 +# +# - platform: tlc59208f +# channel: 1 +# tlc59208f_id: 'tlc59208f_1' +# id: led1 +# +#light: +# - platform: monochromatic +# output: led0 +# name: "LED Geyser Temperature 0" +# id: led_geyser_temp0 +# default_transition_length: 20ms +# +# - platform: monochromatic +# output: led1 +# name: "LED Geyser Temperature 1" +# id: led_geyser_temp1 +# default_transition_length: 20ms + + +binary_sensor: + - platform: status + # Status platform provides a connectivity sensor + name: "Status" + device_class: connectivity -one_wire: - - platform: gpio - pin: GPIO4 - id: geyser_temperature_sensors i2c: sda: GPIO21 @@ -263,1323 +148,38 @@ i2c: id: bus_a frequency: 400kHz -ads1115: +ads1115_int: - address: 0x4A id: ads1115_4A - continuous_mode: true + alert_rdy_pin: + number: GPIO27 + mode: + input: true + pullup: false - address: 0x49 id: ads1115_49 - continuous_mode: true + alert_rdy_pin: + number: GPIO5 + mode: + input: true + pullup: false -ads1115_int: - address: 0x48 id: ads1115_48 - # continuous_mode: true alert_rdy_pin: number: GPIO3 mode: input: true - pullup: true - - - -spi: - - id: spi_bus0 - clk_pin: GPIO18 - mosi_pin: GPIO23 - miso_pin: GPIO19 - interface: any - -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, ','); - - - 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: ds1307 -# # 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: - - 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" - 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: |- - id(record_heat_gained).execute(); - -# # 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"); - - - 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"); - - - 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 - - - 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); - - - interval: 5s - then: - - script.execute: send_battery_info_request - - - 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); - } - -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: - - 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"); - - - platform: gpio - pin: - number: GPIO25 - inverted: true - mode: output - id: led_reset - name: "LED Reset" - icon: "mdi:pool" - restore_mode: ALWAYS_OFF - on_turn_on: - - lambda: |- - ESP_LOGI("led", "LED drive reset"); - - delay: 10ms - - switch.turn_off: led_reset - -tlc59208f: - address: 0x20 - id: tlc59208f_1 - i2c_id: bus_a - -output: -# - platform: ledc -# pin: -# number: GPIO27 -# inverted: true -# id: led0 -# - platform: ledc -# pin: -# number: GPIO26 -# inverted: true -# id: led1 -# - platform: ledc -# pin: -# number: GPIO25 -# inverted: true -# id: led2 -# - platform: ledc -# pin: -# number: GPIO15 -# inverted: true -# id: led3 -# - platform: ledc -# pin: -# number: GPIO1 -# inverted: true -# id: led4 - - platform: ledc - pin: - number: GPIO12 #GPIO26 # LED_LOW_BAT - inverted: false #true - id: led_inverter_battery_low - - - platform: tlc59208f - channel: 0 - tlc59208f_id: 'tlc59208f_1' - id: led0 - - - platform: tlc59208f - channel: 1 - tlc59208f_id: 'tlc59208f_1' - id: led1 - - - platform: tlc59208f - channel: 2 - tlc59208f_id: 'tlc59208f_1' - id: led2 - - - platform: tlc59208f - channel: 3 - tlc59208f_id: 'tlc59208f_1' - id: led3 - - - platform: tlc59208f - channel: 4 - tlc59208f_id: 'tlc59208f_1' - id: led4 - - - platform: tlc59208f - channel: 5 - tlc59208f_id: 'tlc59208f_1' - id: led5 - - - platform: tlc59208f - channel: 6 - tlc59208f_id: 'tlc59208f_1' - id: led6 - - - platform: tlc59208f - 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 + pullup: false # external 10k pullup on ads1115 dev board 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 # ads1115_48 - - platform: ads1115 - multiplexer: 'A2_A3' - gain: 2.048 # 4.096 - ads1115_id: ads1115_49 - name: "ADC Mains Current" - id: mains_current - sample_rate: 475 #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; - - platform: ads1115_int multiplexer: 'A0_A1' gain: 2.048 # 4.096 @@ -1592,23 +192,7 @@ sensor: 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_49 - platform: ads1115_int multiplexer: 'A2_A3' gain: 2.048 # 4.096 @@ -1621,37 +205,50 @@ sensor: accuracy_decimals: 8 unit_of_measurement: "A" icon: "mdi:current" - # update_interval: 8ms #5ms - #filters: - # - 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 #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."); # + 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: 555 #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."); - - platform: ads1115 + # 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" + + - platform: ads1115_int multiplexer: A0_A1 gain: 2.048 # 4.096 ads1115_id: ads1115_49 name: "ADC Lights Current" id: lights_current - sample_rate: 475 #860 + sample_rate: 860 # update_interval: 10ms # id: lights_current_adc state_class: measurement @@ -1660,28 +257,12 @@ sensor: # mod ########################### 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.8 #88.44 - # - offset: 0.0 #-0.2 - # - lambda: |- - # if(abs(x) < 0.1) - # return 0.0; - # return x; - # mod end ####################### -# + # ads1115_4A # Inverter voltage sensor - - platform: ads1115 + - platform: ads1115_int ads1115_id: ads1115_4A - sample_rate: 475 #860 + sample_rate: 860 name: "ADC Mains Voltage" id: mains_voltage_adc unit_of_measurement: "V" @@ -1689,18 +270,18 @@ sensor: icon: "mdi:flash" multiplexer: A0_GND gain: 2.048 # 4.096 - update_interval: 8ms #5ms #23ms + #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 +# 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; @@ -1708,9 +289,9 @@ sensor: # ads1115_4A # Mains voltage sensor - - platform: ads1115 + - platform: ads1115_int ads1115_id: ads1115_4A - sample_rate: 475 #860 + sample_rate: 860 name: "ADC House Voltage" id: inverter_output_voltage_adc unit_of_measurement: "V" @@ -1718,631 +299,49 @@ sensor: icon: "mdi:flash" multiplexer: A2_GND gain: 2.048 # 4.096 - update_interval: 8ms #5ms #23ms + #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 +# 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; - -# -## # 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 + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC Spare1 Voltage" unit_of_measurement: "V" - lambda: |- - return 230.0; - update_interval: 2s + 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 -# 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 + - platform: ads1115_int + ads1115_id: ads1115_4A + sample_rate: 860 + name: "ADC Spare2 Voltage" unit_of_measurement: "V" - lambda: |- - return 230.0; - update_interval: 2s + 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 -# 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 @@ -2378,531 +377,6 @@ sensor: (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 @@ -2910,30 +384,10 @@ text_sensor: name: "Device Info" reset_reason: name: "Reset Reason" - - platform: template - 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 + name: Uptime + id: uptime_human + icon: mdi:clock-start # Expose WiFi information as sensors - platform: wifi_info @@ -2942,1448 +396,3 @@ 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_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: - 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_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 = (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; - double led_1 = 0; - double led_2 = 0; - double led_3 = 0; - double led_4 = 0; - double led_5 = 0; - double led_6 = 0; - double led_7 = 0; - if (led_top == 0) led_0 = led_top_int * led_on; - if (led_top == 1) led_1 = led_top_int * led_on; - if (led_top == 2) led_2 = led_top_int * led_on; - if (led_top == 3) led_3 = led_top_int * led_on; - if (led_top == 4) led_4 = led_top_int * led_on; - if (led_top == 5) led_5 = led_top_int * led_on; - if (led_top == 6) led_6 = led_top_int * led_on; - if (led_top == 7) led_7 = led_top_int * led_on; - if (led_bot == 0) { - led_0 = led_bot_int * led_on; - if (led_top > 1) led_1 = led_on; - if (led_top > 2) led_2 = led_on; - if (led_top > 3) led_3 = led_on; - if (led_top > 4) led_4 = led_on; - if (led_top > 5) led_5 = led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 1) { - led_1 = led_bot_int * led_on; - if (led_top > 2) led_2 = led_on; - if (led_top > 3) led_3 = led_on; - if (led_top > 4) led_4 = led_on; - if (led_top > 5) led_5 = led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 2) { - led_2 = led_bot_int * led_on; - if (led_top > 3) led_3 = led_on; - if (led_top > 4) led_4 = led_on; - if (led_top > 5) led_5 = led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 3) { - led_3 = led_bot_int * led_on; - if (led_top > 4) led_4 = led_on; - if (led_top > 5) led_5 = led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 4) { - led_4 = led_bot_int * led_on; - if (led_top > 5) led_5 = led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 5) { - led_5 = led_bot_int * led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 6) { - led_6 = led_bot_int * led_on; - if (led_top > 6) led_6 = led_on; - if (led_top > 7) led_7 = led_on; - } - if (led_bot == 7) { - led_7 = led_bot_int * led_on; - if (led_top > 7) led_7 = 1; - } - id(led_geyser_temp0).turn_on().set_brightness(led_0).perform(); - id(led_geyser_temp1).turn_on().set_brightness(led_1).perform(); - id(led_geyser_temp2).turn_on().set_brightness(led_2).perform(); - id(led_geyser_temp3).turn_on().set_brightness(led_3).perform(); - 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 ", 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