packages: - !include common/wifi.yaml - !include common/canbus.yaml - !include common/geyser.yaml - !include common/felicityinverter.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] # - priority: 200 # Network connections like MQTT/native API are set up at this priority. # then: # - lambda: |- # - priority: 400 # then: # - switch.turn_on: level_shifter_output_enable globals: - 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 framework: type: esp-idf #arduino # esp-idf debug: update_interval: 15s # Enable logging logger: level: INFO initial_level: INFO logs: canbus: INFO uart: INFO sensor: INFO ads1115.sensor: INFO modbus: INFO # Enable Home Assistant API api: encryption: key: "lcdZmQW414LxtbHNpPpQkM1AyDnCKEYsGSy2c4TlodU=" ota: - platform: esphome password: "0f2e92e0c8764309d5de28191914f0ff" wifi: power_save_mode: none manual_ip: static_ip: 10.0.2.8 # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "${name} Fallback Hotspot" password: "h7BEJBrnZKSQ" captive_portal: one_wire: - platform: gpio pin: GPIO4 id: geyser_temperature_sensors i2c: sda: GPIO21 scl: GPIO22 scan: true id: bus_a frequency: 10kHz ads1115: - address: 0x48 id: ads1115_48 continuous_mode: true - address: 0x49 id: ads1115_49 continuous_mode: true - address: 0x4A id: ads1115_4A continuous_mode: true # - address: 0x4B # id: ads1115_4B 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: |- // id(level_shifter_output_enable).turn_on(); // id(get_ha_settings).execute(); //id(update_power_counters).execute(); //ESP_LOGI("info", "Mains Voltage: %f", id(mains_voltage_adc).state); //ESP_LOGI("info", "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, id(geyser_top_temperature).state); //ESP_LOGI("info", "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, id(geyser_top_temperature).state); - 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 - script.execute: set_heat_indicators - 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 #LISTENONLY 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: - platform: restart name: "${name} Restart" id: "restart_switch" # - platform: gpio # id: level_shifter_output_enable # pin: # number: GPIO12 # inverted: false # mode: # output: true # pullup: true # restore_mode: ALWAYS_OFF - 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: |- id(geyser_relay_status) = true; // only set to false by other sensor / script to include hysteresis and thus avoid relay chattering ESP_LOGI("info", "Geyser Relay turned on"); on_turn_off: - lambda: |- ESP_LOGI("info", "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("info", "Pool Relay turned on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Pool Relay turned off"); # ##tlc59208f: ## address: 0x40 ## id: tlc59208f_1 # output: - platform: ledc pin: number: GPIO27 inverted: true id: led_geyser_temp_blue - platform: ledc pin: number: GPIO26 inverted: true id: led_geyser_temp_green - platform: ledc pin: number: GPIO25 inverted: true id: led_geyser_temp_yellow - platform: ledc pin: number: GPIO15 inverted: true id: led_geyser_temp_orange - platform: ledc pin: number: GPIO1 inverted: true id: led_geyser_temp_red - 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: led_geyser_temp_blue ## ## - platform: tlc59208f ## channel: 1 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_temp_green ## ## - platform: tlc59208f ## channel: 2 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_temp_yellow ## ## - platform: tlc59208f ## channel: 3 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_temp_orange ## ## - platform: tlc59208f ## channel: 4 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_temp_red ## ## - platform: tlc59208f ## channel: 5 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_mode_0 ## ## - platform: tlc59208f ## channel: 6 ## tlc59208f_id: 'tlc59208f_1' ## id: led_geyser_mode_1 ## ## - platform: tlc59208f ## channel: 7 ## tlc59208f_id: 'tlc59208f_1' ## id: led_inverter_battery_low # light: - platform: monochromatic output: led_geyser_temp_blue name: "LED Geyser Temperature Blue" id: led_geyser_temp1 on_turn_on: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED BLUE on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED BLUE off"); - platform: monochromatic output: led_geyser_temp_green name: "LED Geyser Temperature Green" id: led_geyser_temp2 on_turn_on: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED GREEN on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED GREEN off"); - platform: monochromatic output: led_geyser_temp_yellow name: "LED Geyser Temperature Yellow" id: led_geyser_temp3 on_turn_on: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED YELLOW on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED YELLOW off"); - platform: monochromatic output: led_geyser_temp_orange name: "LED Geyser Temperature Orange" id: led_geyser_temp4 on_turn_on: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED ORANGE on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED ORANGE off"); - platform: monochromatic output: led_geyser_temp_red name: "LED Geyser Temperature Red" id: led_geyser_temp5 on_turn_on: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED RED on"); on_turn_off: - lambda: |- ESP_LOGI("info", "Geyser Temperature LED RED 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("info", "Battery Low"); # on_turn_off: # - lambda: |- # ESP_LOGI("info", "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 # 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: 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("info", "Inverter 1 & 2 are being overloaded. Heavy loads should be switched off."); # - switch.turn_off: geyser_relay on_release: then: - lambda: |- ESP_LOGI("info", "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("info", "%d ########### holiday check!: %d/%d ###########", i, id(holidays)[i][0], id(holidays)[i][1]); i++; } // ESP_LOGI("info", "%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 >= 95 && id(battery_charging).state; bool pv_adequate = id(battery_charging_rate).state > 70.0; // id(pv_input_power).state > id(house_power_draw).state; double surplus = sun_high_enough && (battery_full || battery_getting_full || pv_adequate); //ESP_LOGI("solar", "Solar Power surplus? : %s", surplus ? "Yes" : "No"); return surplus; sensor: - 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: 'A0_A1' gain: 2.048 # 4.096 ads1115_id: ads1115_48 sample_rate: 128 #860 # update_interval: 10ms # id: mains_current_adc state_class: measurement device_class: current accuracy_decimals: 8 # mod ########################### name: "Mains Current" id: mains_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: 625 #1250 #5000 send_every: 104 #208 #416 send_first_at: 104 #208 #416 - lambda: return sqrt(x); - multiply: 95.5 #88.44 - offset: -0.2 - lambda: |- if(abs(x) < 0.1) return 0.0; return x; # mod end ####################### - platform: ads1115 multiplexer: 'A2_A3' gain: 2.048 # 4.096 ads1115_id: ads1115_48 sample_rate: 128 #860 # update_interval: 10ms # id: power_outlets_current_adc state_class: measurement device_class: current accuracy_decimals: 8 # mod ########################### name: "Plugs Supply Current" id: power_outlets_current unit_of_measurement: "A" icon: "mdi:current" update_interval: 8ms #5ms filters: # - offset: 0.0002 - lambda: return x * x; - sliding_window_moving_average: window_size: 625 #1250 #5000 send_every: 104 #208 #416 send_first_at: 104 #208 #416 - lambda: return sqrt(x); - multiply: 95 #88.44 - offset: -0.2 - lambda: |- if(abs(x) < 0.1) return 0.0; return x; # mod end ####################### # ads1115_49 - platform: ads1115 multiplexer: 'A0_A1' gain: 2.048 # 4.096 ads1115_id: ads1115_49 sample_rate: 128 #860 # update_interval: 10ms # id: geyser_current_adc state_class: measurement device_class: current accuracy_decimals: 8 # mod ########################### name: "Geyser Current" id: geyser_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: 625 #1250 #5000 send_every: 104 #208 #416 send_first_at: 104 #208 #416 - lambda: return sqrt(x); - multiply: 169.4 #91.1 #88.44 - offset: -0.2 - lambda: |- if(abs(x) < 0.1) return 0.0; return x; on_value_range: - below: 5.0 then: - lambda: |- ESP_LOGI("info", "No geyser current detected. Geyser not heating."); - above: 0.5 then: - lambda: |- ESP_LOGI("info", "Geyser current detected. Geyser was energised."); # mod end ####################### - platform: ads1115 multiplexer: A2_A3 gain: 2.048 # 4.096 ads1115_id: ads1115_49 sample_rate: 128 #860 # update_interval: 10ms # id: lights_current_adc state_class: measurement device_class: current accuracy_decimals: 8 # mod ########################### name: "Lights Current" id: lights_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: 625 #1250 #5000 send_every: 104 #208 #416 send_first_at: 104 #208 #416 - lambda: return sqrt(x); - multiply: 92.1 #88.44 - offset: -0.2 - lambda: |- if(abs(x) < 0.1) return 0.0; return x; # mod end ####################### # ads1115_4A # Mains voltage sensor - platform: ads1115 ads1115_id: ads1115_4A sample_rate: 128 #860 name: "Mains Voltage ADC" id: mains_voltage_adc unit_of_measurement: "V" accuracy_decimals: 8 icon: "mdi:flash" multiplexer: A0_A1 gain: 2.048 # 4.096 update_interval: 8ms #5ms #23ms device_class: voltage state_class: measurement filters: - offset: 0.0065 - lambda: return x * x; - sliding_window_moving_average: window_size: 625 #1250 send_every: 104 send_first_at: 104 #416 - lambda: return sqrt(x); - multiply: 930 #650 - lambda: |- if(abs(x) < 20) return 0; return x; # ads1115_4A # Inverter voltage sensor - platform: ads1115 ads1115_id: ads1115_4A sample_rate: 128 #860 name: "Inverter Output Voltage ADC" id: inverter_output_voltage_adc unit_of_measurement: "V" accuracy_decimals: 8 icon: "mdi:flash" multiplexer: A2_A3 gain: 2.048 # 4.096 update_interval: 8ms #5ms #23ms device_class: voltage state_class: measurement filters: - offset: 0.0131 - lambda: return x * x; - sliding_window_moving_average: window_size: 625 #1250 send_every: 104 send_first_at: 104 - lambda: return sqrt(x); - multiply: 930 #650 - lambda: |- if(abs(x) < 10) return 0; return x; # # ads1115_4A ## # Inverter voltage sensor ## - platform: ads1115 ## ads1115_id: ads1115_4A ## sample_rate: 860 ## name: "ADS1115 4A A2" ## id: adc4A_A2 ## unit_of_measurement: "V" ## accuracy_decimals: 8 ## icon: "mdi:flash" ## multiplexer: A2_GND ## gain: 4.096 ## update_interval: 23ms ## device_class: voltage ## state_class: measurement ## filters: ## - offset: -1.6249 # -1.266 ## - lambda: return x * x; ## - sliding_window_moving_average: ## window_size: 1250 ## send_every: 104 ## send_first_at: 104 ## - lambda: return sqrt(x); ## - multiply: 10000 ## ## # ads1115_4A ## # Inverter voltage sensor ## - platform: ads1115 ## ads1115_id: ads1115_4A ## sample_rate: 860 ## name: "ADS1115 4A A3" ## id: adc4A_A3 ## unit_of_measurement: "V" ## accuracy_decimals: 8 ## icon: "mdi:flash" ## multiplexer: A3_GND ## gain: 4.096 ## update_interval: 23ms ## device_class: voltage ## state_class: measurement ## filters: ## - offset: -1.6249 # -1.266 ## - lambda: return x * x; ## - sliding_window_moving_average: ## window_size: 1250 ## send_every: 104 ## send_first_at: 104 ## - lambda: return sqrt(x); ## - multiply: 10000 # ## # 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("info", "Geyser lost power."); ## - above: 0.5 ## then: ## - lambda: |- ## ESP_LOGI("info", "Geyser was energised."); # ## # 30A clamp ## - platform: ct_clamp ## sensor: lights_current_adc ## id: lights_current ## name: "Lights Current" ## update_interval: 1s ## sample_duration: 1s #15000ms ## state_class: measurement ## device_class: current ## filters: ## # burden resistor is 62Ω in parallel with 33Ω = 21.54Ω ## # multiplier should be 1860/21.54 = x86.35 ## - multiply: 88.44 # real world ## - lambda: |- ## if(x < 0.25) ## return 0.0; ## return x; # ## # 100A clamp ## - platform: ct_clamp ## sensor: mains_current_adc ## id: mains_current ## name: "Mains Current" ## update_interval: 1s ## sample_duration: 1s #15000ms ## state_class: measurement ## device_class: current ## filters: ## # burden resistor is 22Ω ## # multiplier should be 2000/22 = x90.9 ## - multiply: 90.25 # real world ## - lambda: |- ## if(x < 0.25) ## return 0.0; ## return x; ## ## # 100A clamp ## - platform: ct_clamp ## sensor: power_outlets_current_adc ## id: power_outlets_current ## name: "Plugs Supply Current" ## update_interval: 1s ## sample_duration: 1s #15000ms ## state_class: measurement ## device_class: current ## filters: ## # burden resistor is 22Ω ## # multiplier should be 2000/22 = x90.9 ## - multiply: 91.14 # real world ## - lambda: |- ## if(x < 0.25) ## return 0.0; ## return x; # ## - platform: template ## id: calibrate_lights ## name: "AAA Lights A" ## lambda: |- ## return id(lights_current).state; ## state_class: measurement ## device_class: current ## accuracy_decimals: 8 ## update_interval: 1s ## ## - platform: template ## id: calibrate_mains ## name: "AAA Mains A" ## lambda: |- ## return id(mains_current).state; ## state_class: measurement ## device_class: current ## accuracy_decimals: 8 ## update_interval: 1s ## ## - platform: template ## id: calibrate_mains_v ## name: "AAA Mains V" ## lambda: |- ## return id(mains_voltage_adc).state; ## state_class: measurement ## device_class: voltage ## accuracy_decimals: 8 ## update_interval: 1s ## ## - platform: template ## id: calibrate_plugs ## name: "AAA Plugs A" ## lambda: |- ## return id(power_outlets_current).state; ## state_class: measurement ## device_class: current ## accuracy_decimals: 8 ## update_interval: 1s ## ## - platform: template ## id: calibrate_plugs_V ## name: "AAA Plugs V" ## lambda: |- ## return id(inverter_output_voltage_adc).state; ## state_class: measurement ## device_class: voltage ## accuracy_decimals: 8 ## update_interval: 1s ## ## - platform: template ## id: calibrate_geyser ## name: "AAA Geyser A" ## lambda: |- ## return id(geyser_current).state; ## state_class: measurement ## device_class: current ## accuracy_decimals: 8 ## update_interval: 1s # # for now we use a template until we get a voltage sensor - platform: template id: mains_voltage name: "Mains Voltage" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "V" lambda: |- return 230.0; update_interval: 2s device_class: voltage state_class: measurement # for now we use a template until we get a voltage sensor - platform: template id: lights_voltage name: "Lights Voltage" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "V" lambda: |- return 230.0; update_interval: 2s device_class: voltage state_class: measurement # for now we use a template until we get a voltage sensor - platform: template id: inverter1_2_output_voltage name: "Inverter 1 & 2 Output Voltage" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "V" lambda: |- return 230.0; update_interval: 2s device_class: voltage state_class: measurement - platform: template # # if no current is flowing to estimate heating time id: geyser_element_power unit_of_measurement: "W" name: "Geyser Element Power" lambda: |- return 3000.0; device_class: power state_class: measurement - platform: template id: inverter1_2_output_current name: "Inverter 1 & 2 Output Current" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "A" lambda: |- return id(power_outlets_current).state + id(geyser_current).state; update_interval: 2s device_class: current state_class: measurement - platform: template id: inverter1_2_output_power name: "Inverter 1 & 2 Output Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- return 0.001 * (id(inverter1_2_output_voltage).state * id(inverter1_2_output_current).state); update_interval: 2s device_class: power state_class: measurement - platform: template id: geyser_power name: "Geyser Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" filters: - filter_out: nan lambda: |- return 0.001 * id(inverter1_2_output_voltage).state * id(geyser_current).state; update_interval: 2s device_class: power state_class: measurement - platform: template id: power_outlets_power name: "Plugs Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" filters: - filter_out: nan lambda: |- return 0.001 * (id(inverter1_2_output_voltage).state * id(power_outlets_current).state); update_interval: 2s device_class: power state_class: measurement - platform: template id: lights_power name: "Lights Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- return 0.001 * (id(lights_voltage).state * id(lights_current).state); update_interval: 2s device_class: power state_class: measurement - platform: template id: total_inverter_output name: "Total Inverter Output" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- return id(lights_power).state + id(power_outlets_power).state + id(geyser_power).state; update_interval: 2s device_class: power state_class: measurement - platform: template id: mains_power name: "Mains Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- return 0.001 * (id(mains_voltage).state * id(mains_current).state); update_interval: 2s device_class: power state_class: measurement - platform: template id: generated_power name: "Generated Power" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- auto power = id(total_inverter_output).state - id(mains_power).state; if(power < 0) return 0.0; return power; update_interval: 2s device_class: power state_class: measurement - platform: template id: power_loss name: "Power Loss" # icon: mdi:flash accuracy_decimals: 2 unit_of_measurement: "kW" lambda: |- auto power = id(total_inverter_output).state - id(mains_power).state; if(power < 0) return -power; return 0.0; update_interval: 2s device_class: power state_class: measurement - platform: homeassistant entity_id: input_number.geyser_target_temp id: geyser_target_temp - platform: 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Geyser Energy' id: yearly_geyser_energy sensor: geyser_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 # lifetime integration sensor - platform: integration name: 'Geyser Energy' id: geyser_energy sensor: geyser_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Plugs Energy' id: yearly_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 # lifetime integration sensor - platform: integration name: 'Plugs Energy' id: 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 - 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Mains Energy' id: yearly_mains_energy sensor: mains_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 # lifetime integration sensor - platform: integration name: 'Mains Energy' id: mains_energy sensor: mains_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Lights Energy' id: yearly_lights_energy sensor: lights_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 # lifetime integration sensor - platform: integration name: 'Lights Energy' id: lights_energy sensor: lights_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Generated Energy' id: yearly_generated_energy sensor: generated_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 # lifetime integration sensor - platform: integration name: 'Generated Energy' id: generated_energy sensor: generated_power time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly House Energy Usage' id: yearly_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 # lifetime integration sensor - platform: integration name: 'House Energy Usage' id: 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 - 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 # 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 # yearly integration sensor - platform: integration name: 'Yearly Energy Loss' id: yearly_energy_loss sensor: power_loss time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 # # lifetime integration sensor - platform: integration name: 'Energy Loss' id: energy_loss sensor: power_loss time_unit: h restore: true state_class: total_increasing device_class: energy unit_of_measurement: 'kWh' accuracy_decimals: 3 - platform: template id: heating_loss name: "Heat Loss (now)" icon: mdi:thermometer unit_of_measurement: "W" lambda: |- return id(g_heat_loss); update_interval: 2s - platform: template id: active_schedule_day name: "Schedule Day" icon: mdi:calendar-clock accuracy_decimals: 0 unit_of_measurement: "" lambda: |- auto time_obj = ESPTime::from_epoch_local(id(active_schedule_period)[0]); return time_obj.day_of_week; update_interval: 2s - platform: template id: active_schedule_temp name: "Schedule Temp" icon: mdi:water-thermometer-outline unit_of_measurement: "°C" lambda: |- return id(active_schedule_temperature); update_interval: 2s - platform: template id: heat_gained name: "Heat gained" icon: mdi:water-thermometer-outline unit_of_measurement: "W" lambda: |- return id(g_heat_gained); update_interval: 2s - platform: template id: calculated_heat_loss name: "Heat loss (est)" icon: mdi:water-thermometer-outline unit_of_measurement: "W" lambda: |- double dtemp = id(last_temp_diff); if(dtemp < -100) return 0; return id(thermal_transmittance) * id(geyser_surface_area) * id(last_temp_diff); update_interval: 2s - platform: template id: last_geyser_top_temp name: "Last temperature" icon: mdi:water-thermometer-outline unit_of_measurement: "°C" lambda: |- return id(last_geyser_top_temperature); update_interval: 2s device_class: temperature state_class: measurement - platform: dallas_temp address: 0x2e00000059db6928 name: "Geyser Top Temperature" id: geyser_top_temperature update_interval: "60s" resolution: 12 one_wire_id: geyser_temperature_sensors unit_of_measurement: "°C" #icon: "mdi:water-thermometer" device_class: "temperature" state_class: "measurement" accuracy_decimals: 1 filters: - filter_out: nan # - sliding_window_moving_average: # window_size: 120 # averages over 120 update intervals # send_every: 60 # reports every 60 update intervals - platform: dallas_temp address: 0x0b00000036f14d28 name: "Geyser Bottom Temperature" id: geyser_bottom_temperature update_interval: "60s" resolution: 12 one_wire_id: geyser_temperature_sensors unit_of_measurement: "°C" #icon: "mdi:water-thermometer" device_class: "temperature" state_class: "measurement" accuracy_decimals: 1 filters: - filter_out: nan # - sliding_window_moving_average: # window_size: 120 # averages over 120 update intervals # send_every: 60 # reports every 60 update intervals - platform: dallas_temp address: 0x6455a0d445e8f028 name: "Ambient Temperature" id: ambient_temperature update_interval: "60s" resolution: 12 one_wire_id: geyser_temperature_sensors unit_of_measurement: "°C" #icon: "mdi:water-thermometer" device_class: "temperature" state_class: "measurement" accuracy_decimals: 1 filters: - filter_out: nan # - sliding_window_moving_average: # window_size: 120 # averages over 120 update intervals # send_every: 60 # reports every 60 update intervals # Report wifi signal strength every 5 min if changed - platform: wifi_signal name: WiFi Signal update_interval: 300s filters: - delta: 10% # human readable uptime sensor output to the text sensor above - platform: uptime name: Uptime in Days id: uptime_sensor_days update_interval: 10s on_raw_value: then: - text_sensor.template.publish: id: uptime_human state: !lambda |- int seconds = round(id(uptime_sensor_days).raw_state); int days = seconds / (24 * 3600); seconds = seconds % (24 * 3600); int hours = seconds / 3600; seconds = seconds % 3600; int minutes = seconds / 60; seconds = seconds % 60; auto days_str = std::to_string(days); auto hours_str = std::to_string(hours); auto minutes_str = std::to_string(minutes); auto seconds_str = std::to_string(seconds); return ( (days ? days_str + "d " : "") + (hours ? hours_str + "h " : "") + (minutes ? minutes_str + "m " : "") + (seconds_str + "s") ).c_str(); # number of seconds since midnight # - platform: template # id: time_of_day # name: "Time of day" # accuracy_decimals: 0 # unit_of_measurement: "s" # lambda: |- # auto currenttime = id(time_source).now(); # ESPTime time_obj = currenttime; # time_obj.second = 0; # time_obj.minute = 0; # time_obj.hour = 0; # time_obj.recalc_timestamp_local(); # return currenttime.timestamp - time_obj.timestamp; # update_interval: 10s # SOLAR BATTERY - platform: template id: battery_level name: "Battery Level" accuracy_decimals: 0 unit_of_measurement: "%" state_class: measurement device_class: battery lambda: "{ return id(battery_soc).state; }" - platform: template id: battery_soc name: "Battery SOC" accuracy_decimals: 0 unit_of_measurement: "%" state_class: measurement device_class: battery - platform: template id: battery_soh name: "Battery Health" accuracy_decimals: 0 unit_of_measurement: "%" state_class: measurement device_class: battery - platform: template id: battery_system_voltage name: "Battery Voltage" accuracy_decimals: 2 unit_of_measurement: "V" state_class: measurement device_class: voltage - platform: template id: battery_system_current name: "Battery Current" accuracy_decimals: 1 unit_of_measurement: "A" state_class: measurement device_class: current on_value: then: lambda: |- float rate = 0.0; float current = x; 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 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: current #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: power 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: power 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" id: battery_power 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 device: 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 # Expose WiFi information as sensors - platform: wifi_info ip_address: name: IP 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("info", "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("info", "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("info", "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 then: - lambda: |- double temp_top = id(geyser_top_temperature).state; double temp_bottom = id(geyser_bottom_temperature).state; float led_blue = 0; float led_green = 0; float led_yellow = 0; float led_orange = 0; float led_red = 0; float led_on = 0.75; float brightness = 0; float temp1 = 30; float temp2 = 40; float temp3 = 50; float temp4 = 60; if(temp_bottom < temp1) { brightness = 0.1*(temp1 - temp_bottom); led_blue = brightness > 1 ? 1 : brightness; // blue if(temp_top >= temp4) { brightness = 0.1*(temp_top - temp4); led_red = brightness > 1 ? 1 : brightness; // red led_orange = 1; led_yellow = 1; led_green = 1; } else if(temp_top >= temp3) { led_orange = 0.1*(temp_top - temp3); // orange led_yellow = 1; led_green = 1; } else if(temp_top >= temp2) { led_yellow = 0.1*(temp_top - temp2); // yellow led_green = 1; } else if(temp_top >= temp1) { led_green = 0.1*(temp_top - temp1); // green } } else if(temp_bottom >= temp1 && temp_bottom < temp2) { led_green = 0.1*(temp2 - temp_bottom); // green if(temp_top >= temp4) { brightness = 0.1*(temp_top - temp4); led_red = brightness > 1 ? 1 : brightness; // red led_orange = 1; led_yellow = 1; } else if(temp_top >= temp3) { led_orange = 0.1*(temp_top - temp3); // orange led_yellow = 1; } else if(temp_top >= temp2) { led_yellow = 0.1*(temp_top - temp2); // yellow } } // new else if(temp_bottom >= temp2 && temp_bottom < temp3) { led_yellow = 0.1*(temp3 - temp_bottom); // yellow if(temp_top >= temp4) { brightness = 0.1*(temp_top - temp4); led_red = brightness > 1 ? 1 : brightness; // red led_orange = 1; } else if(temp_top >= temp3) { brightness = 0.1*(temp_top - temp3); led_orange = brightness > 1 ? 1 : brightness; // orange } else if(temp_top >= temp2) { led_yellow = 0.1*(temp_top - temp2); // yellow } } // end of new else if(temp_bottom >= temp3 && temp_bottom < temp4) { led_orange = 0.1*(temp3 - temp_bottom); // orange if(temp_top >= temp4) { brightness = 0.1*(temp_top - temp4); led_red = brightness > 1 ? 1 : brightness; // red } else if(temp_top >= temp3) { led_orange = 0.1*(temp_top - temp3); // orange } } else if(temp_bottom > temp4) { double max_temp = (temp_top > temp_bottom) ? temp_top : temp_bottom; // in case there is something wrong with top temp sensor brightness = 0.1*(max_temp - temp4); // red led_red = brightness > 1 ? 1 : brightness; // red } if(temp_top >= temp4) { double max_temp = (temp_top > temp_bottom) ? temp_top : temp_bottom; // in case there is something wrong with top temp sensor brightness = 0.1*(max_temp - temp4); // red led_red = brightness > 1 ? 1 : brightness; // red } led_blue *= led_on; led_green *= led_on; led_yellow *= led_on; led_orange *= led_on; led_red *= led_on; id(led_geyser_temp_blue).set_level(led_blue); id(led_geyser_temp_green).set_level(led_green); id(led_geyser_temp_yellow).set_level(led_yellow); id(led_geyser_temp_orange).set_level(led_orange); id(led_geyser_temp_red).set_level(led_red); //ESP_LOGI("info", "top: %f, bot: %f, Brightness: led_blue %f, led_green: %f, led_yellow, %f, led_orange, %f, led_red %f", temp_top, temp_bottom, led_blue, led_green, led_yellow, led_orange, led_red); # 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: |- 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); auto currenttime = id(time_source).now(); if(currenttime.is_valid()) { time_t now = currenttime.timestamp; bool relay_on = id(geyser_relay).state; ESP_LOGD("geyser", "Geyser heating is turned %s.", relay_on ? "on" : "off"); if(relay_on) { // GEYSER IS ENERGISED // =================== // if we have a surplus of solar power, geyser will remain on regardless if(id(solar_surplus).state) { ESP_LOGD("info", "----------- Geyser remained on at %f °C due to surplus solar power. Battery charge: %0.1f%%", id(geyser_top_temperature).state, id(battery_soc).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("info", "----------- Past the scheduled heating end at %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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("info", "----------- Heating done"); } if(id(inverter1_2_overload).state) { id(geyser_relay_status) = false; ESP_LOGI("info", "----------- Overload condition. Temperature: %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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("info", "----------- Low inverter battery voltage. Temperature: %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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("info", "----------- No mains and inadequate solar power. Temperature: %f °C. Heating start: %s, end: %s, time: %d, Sun: %f ° elevation", id(geyser_top_temperature).state, 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); } if(id(geyser_relay_status)) { // 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("geyser", "Geyser was turned off at %f °C.", id(geyser_top_temperature).state); } } } # 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: |- 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); auto currenttime = id(time_source).now(); if(currenttime.is_valid()) { time_t now = currenttime.timestamp; bool relay_on = id(geyser_relay).state; ESP_LOGD("geyser", "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("geyser", "Geyser was turned on at %f °C due to surplus solar power. Battery charge: %0.1f%%", id(geyser_top_temperature).state, 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("geyser", "Geyser not turned on due to overload condition. Temperature: %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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("geyser", "Geyser not turned on due to low inverter battery voltage. Temperature: %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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("geyser", "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", id(geyser_top_temperature).state, 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; } 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_LOGV("geyser", "Geyser is turned on at %f °C. Heating start: %s, end: %s, time: %d", id(geyser_top_temperature).state, 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 if(relay_on) { double solar_power = id(generated_power).state; double geyser_power = id(geyser_element_power).state; // GEYSER IS ENERGISED // =================== if(!sun_high_enough) { id(geyser_relay_status) = false; ESP_LOGV("geyser", "economy mode: sun not high enough, geyser to be turned off at %f °C. Heating start: %s, end: %s, time: %d, solar: %f kW", id(geyser_top_temperature).state, 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), solar_power); } else if(solar_power < geyser_power) { id(geyser_relay_status) = false; ESP_LOGV("geyser", "economy mode: not enough solar energy, geyser turned to be off at %f °C. Heating start: %s, end: %s, time: %d, solar: %f kW", id(geyser_top_temperature).state, 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), 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: |- // estimate expected heat loss double heat_loss = id(thermal_transmittance) * id(geyser_surface_area) * (id(geyser_top_temperature).state - 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 = id(geyser_top_temperature).state - id(geyser_bottom_temperature).state - id(geyser_top_bottom_constraint); double geyser_effective_temperature = (geyser_temp_diff > 0) ? id(geyser_top_temperature).state - geyser_temp_diff : id(geyser_top_temperature).state; 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: |- //ESP_LOGI("info", "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; if(time_elapsed > 0) { // heat gained measurement if(start_time > 0) { double water_temp = id(geyser_top_temperature).state; 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("info", "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: |- //ESP_LOGI("info", "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("info", "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("info", "======== 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("info", "======== 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("info", "======== 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("info", "======== 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("info", "======== 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 = 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 = 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 = 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 = 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 = 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("info", "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; # }