4387 lines
154 KiB
YAML
4387 lines
154 KiB
YAML
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
|
|
- <source/solar/cb_frame.h> # angle brackets ensure file is included above globals in main.cpp. Make sure to use include GUARDS in the file to prevent double inclusion
|
|
- <source/solar/cbf_store.h>
|
|
- <source/solar/cbf_pylon.h>
|
|
- <source/solar/cbf_store_pylon.h>
|
|
- <source/solar/cbf_sthome.h>
|
|
- <source/solar/cbf_store_sthome.h>
|
|
- <source/solar/cbf_cache.h>
|
|
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<uint32_t> >();
|
|
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<uint32_t> >
|
|
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<uint32_t> 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<uint32_t> 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<int16_t>((x[3] << 8) + x[2]); // unit = 0.1A
|
|
id(battery_charge_current_limit).publish_state(value);
|
|
value = 0.1 * static_cast<int16_t>((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<uint16_t>((x[1] << 8) + x[0]);
|
|
id(battery_soc).publish_state(value);
|
|
value = static_cast<uint16_t>((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<int16_t>((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<int16_t>((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<int16_t>((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<double>(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<int>(heating_time);
|
|
double overshoot_period = heating_factor * id(temp_overshoot_allowed);
|
|
id(estimated_heating_overshoot_time) = static_cast<int>(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<int>(temperature * ${HEATING_TEMP_SCALE});
|
|
id(g_schedule)[day_idx][block_idx][1] = static_cast<int>(start_time * 3600);
|
|
id(g_schedule)[day_idx][block_idx][2] = static_cast<int>(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<float>(t) / ${HEATING_TEMP_SCALE};
|
|
float start_time = static_cast<float>(s) / 3600;
|
|
float end_time = static_cast<float>(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 <cmath>
|
|
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<uint32_t>&
|
|
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<uint32_t, int> 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<uint32_t> 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<uint8_t> 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<uint8_t> 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_t >(time_obj.timestamp);
|
|
# time_t start_time = static_cast<time_t >(id(timer_start));
|
|
# double time_elapsed = static_cast<double>(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;
|
|
# } |