From da5effe9224159f4a6c4e131bc2280e99cf58c7d Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 23 Aug 2025 17:02:53 +0200 Subject: [PATCH] first commit --- README.md | 0 common/canbus.yaml | 34 + common/geyser.yaml | 8 + common/wifi.yaml | 21 + secrets.yaml | 20 + sthome-ut1.yaml | 178 +++ sthome-ut10.yaml | 1456 +++++++++++++++++++++++ sthome-ut2.yaml | 394 +++++++ sthome-ut3.yaml | 339 ++++++ sthome-ut4.yaml | 90 ++ sthome-ut6.yaml | 235 ++++ sthome-ut7.yaml | 177 +++ sthome-ut8.yaml | 2795 ++++++++++++++++++++++++++++++++++++++++++++ sthome-ut9.yaml | 1809 ++++++++++++++++++++++++++++ 14 files changed, 7556 insertions(+) create mode 100644 README.md create mode 100644 common/canbus.yaml create mode 100644 common/geyser.yaml create mode 100644 common/wifi.yaml create mode 100644 secrets.yaml create mode 100644 sthome-ut1.yaml create mode 100644 sthome-ut10.yaml create mode 100644 sthome-ut2.yaml create mode 100644 sthome-ut3.yaml create mode 100644 sthome-ut4.yaml create mode 100644 sthome-ut6.yaml create mode 100644 sthome-ut7.yaml create mode 100644 sthome-ut8.yaml create mode 100644 sthome-ut9.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/common/canbus.yaml b/common/canbus.yaml new file mode 100644 index 0000000..576bad5 --- /dev/null +++ b/common/canbus.yaml @@ -0,0 +1,34 @@ +substitutions: + CB_CANBUS_ID1: 0x501 + CB_CANBUS_ID2: 0x502 + CB_CANBUS_ID3: 0x503 + CB_CANBUS_ID4: 0x504 + CB_CANBUS_ID5: 0x505 + CB_CANBUS_ID6: 0x506 + CB_CANBUS_ID7: 0x507 + CB_CANBUS_ID8: 0x508 + CB_CANBUS_ID9: 0x509 + CB_CANBUS_ID10: 0x50A + CB_GEYSER_ENERGISED: 0x400 + CB_UTILITY_POWER_ON: 0x401 + CB_GEYSER_TOP_TEMPERATURE: 0x410 + CB_GEYSER_BOTTOM_TEMPERATURE: 0x411 + CB_AMBIENT_TEMPERATURE: 0x412 + CB_MAINS_VOLTAGE: 0x420 + CB_INVERTER_OUTPUT_VOLTAGE: 0x421 + CB_MAINS_CURRENT: 0x430 + CB_INVERTER_OUTPUT_CURRENT: 0x431 + CB_GEYSER_CURRENT: 0x432 + CB_POOL_CURRENT: 0x433 + CB_MAINS_POWER: 0x440 + CB_INVERTER_POWER: 0x441 + CB_GEYSER_POWER: 0x442 + CB_POOL_POWER: 0x443 + +# battery messages + CB_BATTERY_LIMITS: 0x351 + CB_BATTERY_STATE: 0x355 + CB_BATTERY_STATUS: 0x356 + CB_BATTERY_FAULT: 0x359 + CB_BATTERY_REQUEST_FLAG: 0x35C + CB_BATTERY_MANUFACTURER: 0x35E diff --git a/common/geyser.yaml b/common/geyser.yaml new file mode 100644 index 0000000..6a1f1fa --- /dev/null +++ b/common/geyser.yaml @@ -0,0 +1,8 @@ +substitutions: + HEATING_IDLE: 30 + HEATING_WARM: 45 + HEATING_LUKE_WARM: 35 + HEATING_HOT: 70 + HEATING_DAY_TYPES: 4 + HEATING_DAY_BLOCKS: 6 + HEATING_TEMP_SCALE: 1000.0 diff --git a/common/wifi.yaml b/common/wifi.yaml new file mode 100644 index 0000000..f7fd518 --- /dev/null +++ b/common/wifi.yaml @@ -0,0 +1,21 @@ +wifi: + domain: .sthome.org + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, + # NB! set a static IP address in main config file + gateway: 10.0.0.2 + subnet: 255.255.240.0 + # we will use local dns server for local dns resolution + dns1: 10.0.0.1 + dns2: 10.0.0.2 diff --git a/secrets.yaml b/secrets.yaml new file mode 100644 index 0000000..cb97272 --- /dev/null +++ b/secrets.yaml @@ -0,0 +1,20 @@ +# Wi-Fi SSID and password (default - used with new device setups) +# for multi network +wifi_ssid1: "sthome-2.4G1" +wifi_ssid2: "sthome-2.4G2" +wifi_ssid3: "sthome-2.4G3" +wifi_ssid4: "sthome-2.4G4" +wifi_ssid5: "sthome-2.4G5" +wifi_password1: "053d53f24e88d8618b618a2fa4c1d9" +wifi_password2: "053d53f24e88d8618b618a2fa4c1d9" +wifi_password3: "053d53f24e88d8618b618a2fa4c1d9" +wifi_password4: "053d53f24e88d8618b618a2fa4c1d9" +wifi_password5: "053d53f24e88d8618b618a2fa4c1d9" + +# default Wi-Fi SSID and password +wifi_ssid: "sthome-2.4G1" +wifi_password: "053d53f24e88d8618b618a2fa4c1d9" + +# latitude & longitude +latitude: -25.711340 +longitude: 28.118370 diff --git a/sthome-ut1.yaml b/sthome-ut1.yaml new file mode 100644 index 0000000..35a9980 --- /dev/null +++ b/sthome-ut1.yaml @@ -0,0 +1,178 @@ +esphome: + name: sthome-ut1 + friendly_name: sthome-ut1 + +esp32: + board: esp32dev + framework: + type: arduino + +# Enable logging +logger: + level: INFO + +# Enable Home Assistant API +api: + encryption: + key: "0Ki4j7JX8Y1IkMGh1fBBu2Dg6DgrnUq8GEXKZvkzeSY=" + +ota: + - platform: esphome + password: "37f546590fcc15e1323d273540eb623a" + +wifi: + #ssid: !secret wifi_ssid + #password: !secret wifi_password + # we will use local dns server for local dns resolution + domain: ".sthome.org" + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, set a static IP address + # Set this to the IP of the ESP + static_ip: 10.0.2.1 + gateway: 10.0.0.2 + subnet: 255.255.240.0 + dns1: 10.0.0.1 + dns2: 10.0.0.2 + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "sthome-ut1 Fallback Hotspot" + password: "7SglIlgdkpAD" + +captive_portal: + +#preferences: +# flash_write_interval: 30s + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: + - platform: homeassistant + +#define OUTPUT_R1 16 +#define OUTPUT_R2 17 +#define OUTPUT_R3 18 +#define OUTPUT_R4 19 +switch: + - platform: gpio + pin: + number: GPIO16 + inverted: true + id: relay1 + name: "Floodlights Backyard" + icon: "mdi:light-flood-down" + restore_mode: RESTORE_DEFAULT_OFF + # the backyard floodlight auto turns off in 4min 14 sec. So we need to switch relay off just before or at this time + # TODO: remove or extend auto turn off to >= 10min + on_turn_on: + - delay: 250s + - switch.turn_off: relay1 + + - platform: gpio + pin: + number: GPIO17 + inverted: true + id: relay2 + name: "Relay 2" + icon: "mdi:run-fast" + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 1000ms + - switch.turn_off: relay2 + + - platform: gpio + pin: + number: GPIO18 + inverted: true + id: relay3 + name: "Relay 3" + icon: "mdi:run-fast" + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 1000ms + - switch.turn_off: relay3 + + - platform: gpio + pin: + number: GPIO19 + inverted: true + id: relay4 + name: "Alarm Zone 4" + icon: "mdi:alarm-light-outline" + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - if: + condition: + - lambda: |- + double sun_elevation = id(sun_sensor).elevation(); + return (sun_elevation <= -6); // -6° = civil twilight, -12° = nautical twilight, -18° = astronomical twilight + #- sun.is_below_horizon: + then: + - switch.turn_on: relay1 + - if: + condition: + - binary_sensor.is_on: floodlight_test + then: + - switch.turn_on: relay1 + - delay: 30s + - switch.turn_off: relay4 + + +# define DIGITAL_D1 04 +binary_sensor: + - platform: gpio + # device_class: light + id: floodlight_test + pin: + number: GPIO04 + mode: + input: true + pullup: true + filters: + - delayed_off: 100ms + name: "Floodlights Test Mode" + icon: "mdi:lightbulb-on-outline" + +#define ANALOG_A1 33 +#define ANALOG_A2 32 +#define ANALOG_A3 35 +#define ANALOG_A4 34 +#define ANALOG_A5 39 +#define ANALOG_A6 36 +sensor: + - platform: adc + pin: 35 + name: "Alarm Signal" + id: alarm_signal + update_interval: 2000ms + attenuation: 12db + sampling_mode: avg + filters: + - lambda: + if (x >= 3.11) { + return x * 1.60256; + } else if (x <= 0.25) { + return 0; + } else { + return x * 1.51; + } + on_value: + then: + # switch on floodlights + lambda: |- + if (id(alarm_signal).state > 1.5) { + id(relay1).turn_on(); + } diff --git a/sthome-ut10.yaml b/sthome-ut10.yaml new file mode 100644 index 0000000..bb97440 --- /dev/null +++ b/sthome-ut10.yaml @@ -0,0 +1,1456 @@ +packages: + - !include common/wifi.yaml + - !include common/canbus.yaml + +substitutions: + name: sthome-ut10 + friendly_name: "sthome-ut10" + #ALLOWED_CHARACTERS_FULL: " !#%\"'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћ" + ALLOWED_CHARACTERS: " !#%\"'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ•" + DD_MAX_YEARS: "5" + + +globals: + - id: g_month_idx + type: int + restore_value: yes + initial_value: '0' + - id: g_year_idx + type: int + restore_value: yes + initial_value: '0' + - id: g_options_year + type: char[1 + ${DD_MAX_YEARS} * 5] + restore_value: yes +# initial_value: "{2022\n2023\n2024\n2025\n2026}" + - id: g_geyser_heating_on + type: bool + restore_value: no + initial_value: '0' + - id: g_utility_on + type: bool + restore_value: no + initial_value: '0' + - id: g_geyser_top_temperature + type: double + restore_value: yes + initial_value: '0' + - id: g_geyser_bottom_temperature + type: double + restore_value: yes + initial_value: '0' + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +esp32: + board: esp32-s3-devkitc-1 + flash_size: 16MB + framework: + type: esp-idf +# sdkconfig_options: +# CONFIG_ESP32_S3_BOX_BOARD: "y" + +psram: + mode: octal + speed: 80MHz + +# Enable logging +logger: +# level: verbose + +# Enable Home Assistant API +api: + encryption: + key: "NApAUAUGkzfbNhOikFb2k+AIE8LMr7O8Ck15IaszrUU=" + +ota: + - platform: esphome + password: "d669ae14ff964bb0f195e820118af8e3" + +wifi: + power_save_mode: none + manual_ip: + static_ip: 10.0.2.10 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "YmuU9F5YgBZ1" + +captive_portal: + +time: + - platform: homeassistant + id: time_source + update_interval: 360min # Change sync interval from default 5min to 6 hours + on_time_sync: + then: +# - if: # Publish the time the device was last restarted, but only once. +# condition: +# lambda: 'return id(device_last_restart).state == "";' +# then: +# - text_sensor.template.publish: +# id: device_last_restart +# state: !lambda 'return id(time_source).now().strftime("%a %d %b %Y - %I:%M:%S %p");' + - script.execute: ind_heating_update +# - script.execute: time_update +# - script.execute: init_calendar + + on_time: + - minutes: '*' + seconds: '*' + then: + - script.execute: ind_heating_update +# - script.execute: time_update +# - lambda: |- +# id(get_calendar_days_state).execute("T"); + #- script.execute: get_calendar_days_state +# +# - hours: 1,2,3,4 +# minutes: 5 +# seconds: 0 +# then: +# - switch.turn_on: switch_antiburn +# - hours: 1,2,3,4 +# minutes: 35 +# seconds: 0 +# then: +# - switch.turn_off: switch_antiburn + +font: + - file: "gfonts://Roboto" + id: roboto_200 + size: 200 + bpp: 4 + glyphs: [ + 0123456789,.,°,a,n, + "\u0020", # space + "\u003A", # colon + ] + - file: "gfonts://Roboto" + id: roboto_192 + size: 192 + bpp: 4 + glyphs: [ + 0123456789,.,°,a,n, + "\u0020", # space + "\u003A", # colon + ] + - file: "gfonts://Roboto" + id: geyser_temperature_font2 + size: 60 + bpp: 4 + glyphs: [ + °,C, + ] + - file: "gfonts://Roboto" + id: geyser_temperature_font3 + size: 30 + bpp: 4 + glyphs: [ + b,o,m,p,t, + ] + - file: "fonts/misc/materialdesignicons-webfont.ttf" + id: font_icon_small + size: 24 #45 + glyphs: [ + "\U0000F5A9", + ] + +color: + - id: grey_light + hex: 'e0e0e0' + +image: +# - file: https://esphome.io/_static/favicon-512x512.png +# id: boot_logo +# resize: 200x200 +# type: RGB565 +# transparency: alpha_channel + - file: mdi:fire + id: icon_fire + resize: 100x100 + type: BINARY + - file: mdi:transmission-tower + id: icon_utility + resize: 80x80 + type: BINARY + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +interval: + - interval: 10s + then: + - canbus.send: + canbus_id: canbus_sthome + data: [0x48, 0x45, 0x4C, 0x4C, 0x4F] + - lambda: |- + ESP_LOGI("SND:${CB_CANBUS_ID10}", "HELLO"); + +spi: + - id: spi_bus0 + clk_pin: GPIO12 + mosi_pin: GPIO11 + miso_pin: GPIO13 + interface: any + + - id: spi_bus1 + clk_pin: GPIO42 + mosi_pin: GPIO21 + miso_pin: GPIO20 + interface: any + +#one_wire: +# - platform: gpio +# pin: GPIO4 +# id: temperature_sensors + +# CAN BUS +canbus: + - platform: mcp2515 + cs_pin: GPIO10 + spi_id: spi_bus0 + id: canbus_sthome + can_id: ${CB_CANBUS_ID10} + #mode: NORMAL #LISTENONLY + bit_rate: 500KBPS #20KBPS + on_frame: + - can_id: 0 + can_id_mask: 0 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGI("REC: 0", "%s", &b[0] ); + - can_id: ${CB_GEYSER_ENERGISED} + then: + - lvgl.widget.update: + id: ind_geyser_on + hidden: !lambda |- + std::string on_state(x.begin(), x.end()); + //ESP_LOGI("REC:${CB_GEYSER_ENERGISED}", "GEYSER IS: %s", on_state.c_str()); + if(on_state == "ON") { + id(g_geyser_heating_on) = true; + return false; // not hidden + } + else if(on_state == "OFF") { + id(g_geyser_heating_on) = false; + return true; // hidden + } + //ESP_LOGW("REC:${CB_GEYSER_ENERGISED}", "Invalid ON/OFF value: %s", on_state.c_str()); + return true; // default + - can_id: ${CB_UTILITY_POWER_ON} + then: + - lvgl.widget.update: + id: ind_utility_on + hidden: !lambda |- + std::string on_state(x.begin(), x.end()); + ESP_LOGI("REC:${CB_UTILITY_POWER_ON}", "UTILITY IS: %s", on_state.c_str()); + if(on_state == "ON") { + id(g_utility_on) = true; + return false; // not hidden + } + else if(on_state == "OFF") { + id(g_utility_on) = false; + return true; // hidden + } + ESP_LOGW("REC:${CB_UTILITY_POWER_ON}", "Invalid ON/OFF value: %s", on_state.c_str()); + return true; // default + + - can_id: ${CB_GEYSER_TOP_TEMPERATURE} + then: + - lambda: |- + id(update_temperature_display).execute(x, id(g_geyser_top_temperature), rect_gtoptemp, ind_utility_on, lbl_gtoptemp); + + - can_id: ${CB_GEYSER_BOTTOM_TEMPERATURE} + then: + - lambda: |- + id(update_temperature_display).execute(x, id(g_geyser_bottom_temperature) , rect_gbottemp, ind_geyser_on, lbl_gbottemp); + +# - can_id: ${CB_CANBUS_ID1} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID1}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID2} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID2}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID3} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID3}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID4} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID4}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID5} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID5}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID6} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID6}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID7} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID7}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID8} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID8}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID9} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID9}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID10} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID10}", "%s", &b[0] ); + + +display: + - platform: ili9xxx + model: ili9488 + id: tft_display + color_palette: 8BIT + data_rate: 40MHz + spi_id: spi_bus1 + cs_pin: GPIO41 + dc_pin: GPIO39 + reset_pin: GPIO40 + auto_clear_enabled: false + update_interval: never + invert_colors: false + show_test_card: true + transform: + swap_xy: true # landscape +# mirror_x: true # landscape + dimensions: + height: 480 + width: 320 + +# Define a PWM output on the ESP32 +output: + - platform: ledc + pin: GPIO38 + id: backlight_pwm + +# Define a monochromatic, dimmable light for the backlight +light: + - platform: monochromatic + output: backlight_pwm + name: "Display Backlight" + id: back_light + restore_mode: ALWAYS_ON + +touchscreen: + platform: xpt2046 + id: touch_screen + spi_id: spi_bus1 + cs_pin: GPIO47 + interrupt_pin: GPIO19 + transform: + swap_xy: true # landscape + # mirror_y: true # portrait + calibration: + x_min: 231 #201 #281 + x_max: 3878 #3793 #3848 + y_min: 221 #228 #347 + y_max: 3861 #3914 #3878 + +lvgl: +# color_depth: 16 +# bg_color: 0x0F0F0F + default_font: unscii_8 +# align: center + theme: + button: + bg_color: grey_light #0x2F8CD8 +# bg_grad_color: 0x005782 +# bg_grad_dir: VER + bg_opa: COVER + border_color: 0x0077b3 + border_width: 1 + text_color: 0xFFFFFF + pressed: # set some button colors to be different in pressed state + bg_color: 0x006699 + bg_grad_color: 0x00334d + checked: # set some button colors to be different in checked state + bg_color: 0x1d5f96 + bg_grad_color: 0x03324A + text_color: 0xfff300 +# switch: +# bg_color: 0xC0C0C0 +# bg_grad_color: 0xb0b0b0 +# bg_grad_dir: VER +# bg_opa: COVER +# checked: +# bg_color: 0x1d5f96 +# bg_grad_color: 0x03324A +# bg_grad_dir: VER +# bg_opa: COVER +# knob: +# bg_color: 0xFFFFFF +# bg_grad_color: 0xC0C0C0 +# bg_grad_dir: VER +# bg_opa: COVER +# slider: +# border_width: 1 +# border_opa: 15% +# bg_color: 0xcccaca +# bg_opa: 15% +# indicator: +# bg_color: 0x1d5f96 +# bg_grad_color: 0x03324A +# bg_grad_dir: VER +# bg_opa: COVER +# knob: +# bg_color: 0x2F8CD8 +# bg_grad_color: 0x005782 +# bg_grad_dir: VER +# bg_opa: COVER +# border_color: 0x0077b3 +# border_width: 1 +# text_color: 0xFFFFFF + style_definitions: + - id: header_footer + bg_color: darkgrey #0x2F8CD8 + bg_opa: COVER + border_opa: TRANSP + radius: 0 + pad_all: 0 + pad_row: 0 + pad_column: 0 + border_color: 0x0077b3 + text_color: 0xFFFFFF + width: 100% + height: 30 + - id: clockdate_style + text_font: montserrat_20 #roboto_20 #unscii_8 + text_align: center + text_color: 0x000000 + radius: 4 + pad_all: 2 + - id: sty_calendar_small + radius: 0 + pad_all: 0 + pad_row: 0 + pad_column: 0 + text_font: unscii_8 + shadow_opa: TRANSP + text_color: black + bg_color: white + bg_opa: COVER + border_color: grey_light + border_width: 1 + border_opa: cover #TRANSP + - id: sty_calendar_small_noborders + radius: 0 + pad_all: 0 + pad_row: 0 + pad_column: 0 + text_font: unscii_8 + shadow_opa: TRANSP + text_color: black + bg_color: white + bg_opa: COVER + border_color: grey_light + border_width: 0 + border_opa: cover #TRANSP + displays: + - tft_display + buffer_size: 12% + top_layer: + widgets: + - label: + text: "\U0000F5A9" # "\uF1EB" + id: lbl_hastatus + hidden: true + align: top_right + x: -2 + y: 1 + text_font: font_icon_small #montserrat_16 + text_align: right + text_color: 0x202020 # 0xFFFFFF + - obj: # clipping rectangle + x: 0 #15 + y: -24 #7 + pad_all: 0 + height: 90 + width: 65 + align: BOTTOM_RIGHT + bg_color: 0x000000 + border_color: 0xFFFFFF + border_width: 0 + radius: 0 + bg_opa: LV_OPA_TRANSP + scrollbar_mode: "OFF" + widgets: + - image: + id: ind_geyser_on + align: CENTER #BOTTOM_RIGHT #TOP_RIGHT + src: icon_fire + image_recolor: RED + image_recolor_opa: 100% + x: 0 #15 #15 + y: 0 #-22 #7 + height: 100 #25 + width: 100 #25 + - obj: # clipping rectangle + x: 0 #15 + y: 2 #-24 #7 + pad_all: 0 + height: 80 + width: 65 + align: TOP_LEFT + bg_color: 0x000000 + border_color: 0xFFFFFF + border_width: 0 + radius: 0 + bg_opa: LV_OPA_TRANSP + scrollbar_mode: "OFF" + widgets: + - image: + id: ind_utility_on + align: CENTER #BOTTOM_RIGHT #TOP_RIGHT + src: icon_utility + image_recolor: grey #!lambda 'return lv_color_hex(0x000000);' + image_recolor_opa: 100% + x: 0 #15 #15 + y: 0 #-22 #7 + height: 80 #25 + width: 80 #25 +# - obj: +# id: boot_screen +# x: 0 +# y: 0 +# width: 100% +# height: 100% +# bg_color: 0xffffff +# bg_opa: COVER +# radius: 0 +# pad_all: 0 +# border_width: 0 +# widgets: +# - image: +# align: CENTER +# src: boot_logo +# y: -40 +# - spinner: +# align: CENTER +# y: 95 +# height: 50 +# width: 50 +# spin_time: 1s +# arc_length: 60deg +# arc_width: 8 +# indicator: +# arc_color: 0x18bcf2 +# arc_width: 8 +# on_press: +# - lvgl.widget.hide: boot_screen + - buttonmatrix: + text_font: montserrat_16 + align: bottom_mid + styles: header_footer + pad_all: 0 + outline_width: 0 + id: footer + width: 480 + items: + styles: header_footer + rows: + - buttons: + - id: page_prev + text: "\uF053" + on_press: + then: + lvgl.page.previous: + - id: page_home + text: "\uF015" + on_press: + then: + lvgl.page.show: main_page + - id: page_next + text: "\uF054" + on_press: + then: + lvgl.page.next: + pages: + # - id: pg_calendar + # widgets: + # - button: + # id: cal_btn_prev_month + # styles: sty_calendar_small + # align: TOP_MID + # pad_all: 0 + # outline_width: 0 + # border_color: black + # border_width: 0 #1 + # border_opa: TRANSP + # x: -75 + # y: 30 + # width: 20 + # height: 20 + # bg_color: grey_light + # text_color: 0xD3D3D3 + # text_font: montserrat_14 + # widgets: + # - label: + # align: center + # text_font: montserrat_14 + # text: "<" + # on_press: + # then: + # lambda: |- + # id(update_calendar_month).execute(-1); + # - dropdown: + # id: cal_dd_year + # styles: sty_calendar_small + # text_font: montserrat_12 + # height: 20 + # width: 55 + # radius: 0 + # align_to: + # id: cal_btn_prev_month + # align: out_right_top + # x: 80 + # y: 0 #12.5% + # options: + # - 2024 + # - 2025 + # selected_index: 0 + # dropdown_list: + # text_line_space: 3 + # pad_all: 1 + # text_font: unscii_8 + # max_height: 260 + # radius: 0 + # selected: + # checked: + # text_color: 0xFF0000 + # on_value: + # then: + # - lambda: |- + # id(update_calendar).execute(); + # - dropdown: + # id: cal_dd_month + # styles: sty_calendar_small + # text_font: montserrat_12 + # height: 20 + # width: 55 + # radius: 0 + # align_to: + # id: cal_dd_year + # align: out_right_top + # x: 0 + # y: 0 #12.5% + # options: + # - Jan + # - Feb + # - Mar + # - Apr + # - May + # - Jun + # - Jul + # - Aug + # - Sep + # - Oct + # - Nov + # - Dec + # selected_index: 0 + # dropdown_list: + # text_line_space: 3 + # pad_all: 1 + # text_font: unscii_8 + # max_height: 260 + # radius: 0 + # selected: + # checked: + # text_color: 0xFF0000 + # on_value: + # then: + # - lambda: |- + # id(update_calendar).execute(); + # - button: + # id: cal_btn_next_month + # styles: sty_calendar_small + # align_to: + # id: cal_dd_month + # align: out_right_top + # x: 0 + # y: 0 + # pad_all: 0 + # outline_width: 0 + # border_color: black + # border_width: 0 #1 + # border_opa: TRANSP + # x: -75 + # y: 30 + # width: 20 + # height: 20 + # bg_color: grey_light + # text_color: 0xD3D3D3 + # text_font: montserrat_14 + # widgets: + # - label: + # align: center + # text_font: montserrat_14 + # text: ">" + # on_press: + # then: + # lambda: |- + # id(update_calendar_month).execute(1); + # - buttonmatrix: + # id: bmx_cal_header_dow + # styles: sty_calendar_small_noborders + # align_to: + # id: cal_btn_prev_month + # align: out_bottom_left + # x: 80 + # y: 0 #12.5% + # pad_all: 0 + # outline_width: 0 + # border_color: black + # border_width: 0 #1 + # border_opa: TRANSP + # x: 0 + # y: 0 + # width: 150 + # height: 20 + # bg_color: black + # text_color: 0xD3D3D3 + # items: + # styles: sty_calendar_small_noborders + # pressed: + # bg_color: 0x006699 + # bg_grad_color: 0x00334d + # checked: + # bg_color: 0x1d5f96 + # bg_grad_color: 0x03324A + # rows: + # - buttons: + # - id: r0c1 + # text: "Su" + # width: 1 + # - id: r0c2 + # text: "Mo" + # width: 1 + # - id: r0c3 + # text: "Tu" + # width: 1 + # - id: r0c4 + # text: "We" + # width: 1 + # - id: r0c5 + # text: "Th" + # width: 1 + # - id: r0c6 + # text: "Fr" + # width: 1 + # - id: r0c7 + # text: "Sa" + # width: 1 + # on_press: + # then: + # - lambda: |- + # ESP_LOGI("day of week", "%d", x); + # - buttonmatrix: + # id: bmx_calendar + # styles: sty_calendar_small + # align_to: + # id: bmx_cal_header_dow + # align: out_bottom_left + # x: 0 + # y: 0 #12.5% + # pad_all: 0 + # outline_width: 0 + # border_color: black + # border_width: 0 #1 + # border_opa: TRANSP + # x: 0 + # y: 0 + # width: 150 + # height: 100 + # bg_color: black + # text_color: 0xD3D3D3 + # items: + # styles: sty_calendar_small + # pressed: + # bg_color: 0x006699 + # bg_grad_color: 0x00334d + # checked: + # bg_color: 0x1d5f96 + # bg_grad_color: 0x03324A + # rows: + # - buttons: + # - id: r1c1 + # text: " " + # width: 1 + # control: + # recolor: true + # - id: r1c2 + # text: " " + # width: 1 + # - id: r1c3 + # text: "1" + # width: 1 + # - id: r1c4 + # text: "2" + # width: 1 + # - id: r1c5 + # text: "3" + # width: 1 + # - id: r1c6 + # text: "4" + # width: 1 + # - id: r1c7 + # text: "5" + # width: 1 + + # # Define actions on button press + # on_press: + # then: + # lambda: |- + # //lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); + # //lv_btnmatrix_set_one_checked(bmx_calendar->obj, false); + # //id(get_calendar_days_state).execute("P1"); + # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); + # //ESP_LOGI("on press", "day: %s, stat: %d", lv_btnmatrix_get_btn_text(bmx_calendar->obj, x), stat); + # //id(get_calendar_days_state).execute("P2"); + # on_release: + # then: + # lambda: |- + # id(get_calendar_days_state).execute("R1"); + # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); + # //auto* day = lv_btnmatrix_get_btn_text(bmx_calendar->obj, x); + # // if(stat) { + # // lv_btnmatrix_clear_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); + # // } + # // else { + # // lv_btnmatrix_set_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); + # // } + # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); + # //ESP_LOGI("on relse", "day: %s, stat: %d", lv_btnmatrix_get_btn_text(bmx_calendar->obj, x), stat); + # //id(get_calendar_days_state).execute("R2"); + + - id: main_page #pg_geyser_temp + widgets: + - obj: + id: rect_gtoptemp + x: 0 + y: 0 #30 + pad_all: 0 + height: 290 + width: 240 + align: TOP_LEFT + bg_color: 0x000000 + border_color: 0xFFFFFF + border_width: 0 + radius: 0 + bg_opa: COVER + - obj: + id: rect_gbottemp + y: 0 + pad_all: 0 + height: 290 + width: 240 + align_to: + id: rect_gtoptemp + align: out_right_top + x: 0 + y: 0 #12.5% + bg_color: 0x000000 #0xFF4500 + border_color: 0xFFFFFF + border_width: 0 + radius: 0 + bg_opa: COVER + - label: + text: " " + id: lbl_gtoptemp + hidden: false + align: LEFT_MID + x: 0 + y: -10 + text_font: roboto_200 + text_align: center + text_color: 0x0 + bg_opa: LV_OPA_TRANSP + bg_color: 0xffffff + - label: + text: " " + id: lbl_gbottemp + hidden: false + align: RIGHT_MID + x: 0 + y: -10 + text_font: roboto_200 + text_align: center + text_color: 0x0 + bg_opa: LV_OPA_TRANSP + bg_color: 0xffffff +# - label: +# text: "°C" +# id: lbl_degree +# hidden: false +# align: BOTTOM_MID +# x: 0 +# y: -30 +# text_font: geyser_temperature_font2 +# text_align: center +# text_color: 0x0 +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff + - label: + text: "top" + id: lbl_top + hidden: false + align: TOP_MID + x: -120 + y: 20 + text_font: geyser_temperature_font3 + text_align: center + text_color: 0x0 + bg_opa: LV_OPA_TRANSP + bg_color: 0xffffff + - label: + text: "bottom" + id: lbl_bottom + hidden: false + align: TOP_MID + x: 120 + y: 20 + text_font: geyser_temperature_font3 + text_align: center + text_color: 0x0 + bg_opa: LV_OPA_TRANSP + bg_color: 0xffffff + +# - id: pg_settings +# widgets: +# - textarea: +# id: geyser_schedule +# one_line: true +# placeholder_text: "Enter text here" +# - keyboard: +# id: keyboard_id +# textarea: geyser_schedule +# mode: TEXT_UPPER +# text_font: montserrat_20 +# on_focus: +# then: +# - lvgl.keyboard.update: +# id: keyboard_id +# mode: number +# textarea: geyser_schedule +# on_ready: +# then: +# - logger.log: Keyboard is ready +# on_cancel: +# then: +# - logger.log: Keyboard cancelled# + +# - id: pg_clock +# widgets: +# - obj: # clock container +# height: 300 #SIZE_CONTENT +# width: 300 # 100% +# align: TOP_MID +# pad_all: 0 +# border_width: 0 +# bg_color: 0xFFFFFF +# widgets: +# - meter: # clock face +# height: 300 +# width: 300 +# align: TOP_MID +# bg_opa: TRANSP +# border_width: 0 +# text_color: 0x000000 +# scales: +# - range_from: 0 # minutes scale +# range_to: 720 +# angle_range: 360 +# rotation: 270 +# ticks: +# width: 1 +# count: 61 +# length: 10 +# color: 0x000000 +# indicators: +# - line: +# id: minute_hand +# width: 3 +# color: 0xa6a6a6 +# r_mod: -4 +# value: 0 +# - range_from: 1 # hours scale for labels +# range_to: 12 +# angle_range: 330 +# rotation: 300 +# ticks: +# width: 1 +# count: 12 +# length: 1 +# major: +# stride: 1 +# width: 4 +# length: 10 +# color: 0xC0C0C0 +# label_gap: 12 +# - range_from: 0 # hi-res hours scale for hand +# range_to: 720 +# angle_range: 360 +# rotation: 270 +# ticks: +# count: 0 +# indicators: +# - line: +# id: hour_hand +# width: 5 +# color: 0xa6a6a6 +# r_mod: -30 +# value: 0 +# # Second hand +# - angle_range: 360 +# rotation: 270 +# range_from: 0 +# range_to: 60 +# indicators: +# - line: +# id: second_hand +# width: 2 +# color: Red +# r_mod: -10 +# - label: +# align: CENTER +# styles: clockdate_style +# id: day_label +# y: -50 +# - label: +# align: CENTER +# id: date_label +# styles: clockdate_style +# y: 50 + +# - id: pg_digital_clock +# widgets: +# - obj: +# id: rect_gtoptemp1 +# x: 0 +# y: 0 #30 +# pad_all: 0 +# height: 290 +# width: 240 +# align: TOP_LEFT +# bg_color: 0x000000 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: COVER +# - obj: +# id: rect_gbottemp1 +# y: 0 +# pad_all: 0 +# height: 290 +# width: 240 +# align_to: +# id: rect_gtoptemp +# align: out_right_top +# x: 0 +# y: 0 #12.5% +# bg_color: 0x000000 #0xFF4500 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: COVER +# - label: +# text: " " +# id: lbl_digitalclock +# hidden: false +# align: TOP_MID +# x: 0 +# y: 20 +# text_font: roboto_192 +# text_align: center +# text_color: RED +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff + +switch: + - platform: restart + name: "${name} Restart" + id: "restart_switch" +# - platform: template +# name: Antiburn +# id: switch_antiburn +# icon: mdi:television-shimmer +# optimistic: true +# entity_category: "config" +# turn_on_action: +# - logger.log: "Starting Antiburn" +# - if: +# condition: lvgl.is_paused +# then: +# - lvgl.resume: +# - lvgl.widget.redraw: +# - lvgl.pause: +# show_snow: true +# turn_off_action: +# - logger.log: "Stopping Antiburn" +# - if: +# condition: lvgl.is_paused +# then: +# - lvgl.resume: +# - lvgl.widget.redraw: + +sensor: +# - platform: dallas_temp +# address: 0xfe00000037b3d528 +# name: "Study Temperature" +# id: study_temperature +# update_interval: "60s" +# resolution: 12 +# one_wire_id: 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 + id: wifi_sig + 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: 60s + +#text_sensor: +## - 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: 60s +# +# # Expose WiFi information as sensors +# - platform: wifi_info +# ip_address: +# name: IP +# mac_address: +# name: Mac Address +# entity_category: diagnostic +# ssid: +# name: "Connected SSID" +# id: ssid +# entity_category: diagnostic +# +# # human readable update text sensor from sensor:uptime +# - platform: template +# name: Uptime +# id: uptime_human +# icon: mdi:clock-start +# +# - platform: template +# name: 'Last Restart' +# id: device_last_restart +# icon: mdi:clock +# entity_category: diagnostic +# + +script: + - id: update_temperature_display + parameters: + x: std::vector& + globalvar: double& + rect: lv_obj_t* + indicator: lv_obj_t* + label: lv_obj_t* + then: + - lambda: |- + char buffer [4]; + buffer[0] = '\0'; + double value = x[3] + ((double)((x[2] << 16) + (x[1] << 8) + x[0]))/16777216; + globalvar = value; + snprintf (buffer, 4, "%.0f", value); + auto bgcolor = lv_color_hex(0xFF0000); + auto ind_color = lv_color_hex(0xFF0000); + if(value < 40) { + bgcolor = lv_color_hex(0x0000FF); + } + else if(value < 50) { + bgcolor = lv_color_hex(0x00FF00); + } + else if(value < 60) { + bgcolor = lv_color_hex(0xFFFF00); + } + else { + ind_color = lv_color_hex(0xFFFF00); // make different to bgcolor + } + lv_obj_set_style_bg_color(rect, bgcolor, LV_PART_MAIN); + lv_obj_set_style_img_recolor(indicator, ind_color, LV_PART_MAIN); + lv_label_set_text(label, buffer); + +# - id: time_update +# then: +# - lvgl.indicator.update: +# id: minute_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return now.minute * 12 + now.second/5; +# - lvgl.indicator.update: +# id: hour_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return std::fmod(now.hour, 12) * 60 + now.minute; +# - lvgl.indicator.update: +# id: second_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return now.second; +# - lvgl.label.update: +# id: date_label +# text: !lambda |- +# static const char * const mon_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +# static char date_buf[8]; +# auto now = id(time_source).now(); +# snprintf(date_buf, sizeof(date_buf), "%s %2d", mon_names[now.month-1], now.day_of_month); +# return date_buf; +# - lvgl.label.update: +# id: day_label +# text: !lambda |- +# static const char * const day_names[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; +# return day_names[id(time_source).now().day_of_week - 1]; +# - lvgl.label.update: +# id: lbl_digitalclock +# text: !lambda |- +# auto time_obj = id(time_source).now(); +# return time_obj.strftime("%H:%M"); + + - id: ind_heating_update + then: + - lvgl.widget.update: + id: ind_geyser_on + hidden: !lambda return !id(g_geyser_heating_on); + +# - id: init_calendar +# then: +# - lambda: |- +# auto now = id(time_source).now(); +# //ESP_LOGI("yopts before", stroptions.c_str()); +# int y = 0; +# std::string stroptions = to_string(now.year + y); +# while(++y < ${DD_MAX_YEARS}) { +# stroptions += "\n" + to_string(now.year + y); +# } +# //ESP_LOGI("yopts after", stroptions.c_str()); +# lv_dropdown_set_options(cal_dd_year->obj, stroptions.c_str()); +# lv_dropdown_set_selected(cal_dd_year->obj, 0); // this year is first index +# lv_dropdown_set_selected(cal_dd_month->obj, now.month-1); +# id(g_year_idx) = 0; +# id(update_calendar).execute(); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); + +# - id: update_calendar_month +# parameters: +# increment : int +# then: +# - lambda: |- +# char yearstr[8]; +# lv_dropdown_get_selected_str(cal_dd_year->obj, yearstr, sizeof(yearstr)); +# auto year = atoi(yearstr); +# int year_idx = lv_dropdown_get_selected(cal_dd_year->obj); +# int month_idx = increment + lv_dropdown_get_selected(cal_dd_month->obj); +# int month = 1 + month_idx; +# if(month > 12 && year_idx < ${DD_MAX_YEARS} - 1) { +# month -= 12; +# month_idx -= 12; +# year++; +# year_idx++; +# } +# else if(month < 1 && year_idx > 0) { +# month += 12; +# month_idx += 12; +# year--; +# year_idx--; +# } +# ESP_LOGI("cm", "month: %d, year: %d", month, year); +# if(month < 13 && month > 0) { +# lv_dropdown_set_selected(cal_dd_year->obj, year_idx); +# lv_dropdown_set_selected(cal_dd_month->obj, month_idx); +# id(g_year_idx) = year_idx; +# id(update_calendar).execute(); +# } +# +# - id: update_calendar +# then: +# lambda: |- +# char yearstr[8]; +# int monthdays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +# static std::string strdays[44]; +# static const char *pstrdays[49]; // including newline at end of week +# static const char *newline = "\n"; +# id(persist_calendar).execute(); +# lv_dropdown_get_selected_str(cal_dd_year->obj, yearstr, sizeof(yearstr)); +# int year = atoi(yearstr); +# int month = 1 + lv_dropdown_get_selected(cal_dd_month->obj); +# bool isLeapYear = (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0)); +# monthdays[1] = (isLeapYear) ? 29 : 28; +# // calculate day of week of 1st of month using Zeller's rule +# // https://beginnersbook.com/2013/04/calculating-day-given-date +# // modified month, year +# int mM = month - 2; +# int m = mM < 1 ? 12 + mM : mM; +# int mY = mM < 1 ? year - 1 : year; +# int k = 1; // day of month +# int D = mY % 100; // last two digits of the year +# int C = trunc(mY / 100); // first two digits of the year +# int F = k + trunc((13 * m - 1) / 5) + D + trunc(D / 4) + trunc(C / 4) - 2 * C; +# int Z = F % 7; +# int start_of_month = Z < 0 ? Z + 7 : Z; +# // end of Zeller's rule +# int previous_month = (month == 1) ? 12 : month - 1; +# int month_days = monthdays[month - 1]; +# int prev_month_days = monthdays[previous_month - 1]; +# int i = 0; +# int j = -1; +# //ESP_LOGI("vals", "start_of_month: %d, previous_month: %d, month_days: %d, prev_month_days: %d", start_of_month, previous_month, month_days, prev_month_days); +# for (int w = 0; w < 6 && i < (month_days + start_of_month); w++) { +# for(int wd = 0; wd < 7; wd++) { +# int day = i + 1 - start_of_month; +# if (i < start_of_month) { +# day += prev_month_days; +# strdays[i] = "#e0e0e0 " + to_string(day) + "#"; +# } +# else if (i >= (month_days + start_of_month)) { +# day -= month_days; +# strdays[i] = "#e0e0e0 " + to_string(day) + "#"; +# } +# else { +# strdays[i] = to_string(day); +# } +# pstrdays[++j] = strdays[i].c_str(); +# //ESP_LOGI("bmx", "%s, i: %d, j: %d", pstrdays[j], i, j); +# i++; +# } +# pstrdays[++j] = newline; +# //ESP_LOGI("bmxnl", "%s, i: %d, j: %d", pstrdays[j], i, j); +# } +# pstrdays[j] = NULL; // terminator, overwrites last newline +# //ESP_LOGW("day", "terminating at: %d", j); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); +# lv_btnmatrix_set_map(bmx_calendar->obj, pstrdays); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); +# +# - id: persist_calendar +# then: +# lambda: |- +# id(g_year_idx) = lv_dropdown_get_selected(cal_dd_year->obj); +# id(g_month_idx) = lv_dropdown_get_selected(cal_dd_month->obj); +# // copy year options to persistent globals +# const char* opts = lv_dropdown_get_options(cal_dd_year->obj); +# int opt_store_size = sizeof(id(g_options_year)); +# strncpy(id(g_options_year), opts, opt_store_size); +# id(g_options_year)[opt_store_size] = '\0'; +# //ESP_LOGI("year options", id(g_options_year)); +# +# - id: get_calendar_days_state +# parameters: +# flag: std::string +# then: +# lambda: |- +# // count buttons +# int num_buttons = 0; +# auto* buttonmap = lv_btnmatrix_get_map(bmx_calendar->obj); +# int i = 0; +# for (; buttonmap[i] != NULL && buttonmap[i][0] != '\0' && i < 48; i++) { +# bool isNewLine = strcmp(buttonmap[i], "\n") == 0; +# if (!isNewLine) { +# num_buttons++; +# } +# } +# std::string sch_holidays = ""; +# std::string pub_holidays = ""; +# std::string vac_days = ""; +# int h = 0; +# for(int i = 0; i < num_buttons; i++) { +# bool isSchoolHoliday = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CHECKED); +# bool isPublicHoliday = false; //lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CUSTOM_1); +# bool isVacationDay = false; //lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CUSTOM_2); +# if(isSchoolHoliday || isPublicHoliday || isVacationDay) { +# sch_holidays = sch_holidays + lv_btnmatrix_get_btn_text(bmx_calendar->obj, i) + " "; +# h++; +# } +# } +# if(h > 0) { +# ESP_LOGI("day", "[%s] s: %s \tp: %s \tv: %s", flag.c_str(), sch_holidays.c_str(), pub_holidays.c_str(), vac_days.c_str()); +# } + + \ No newline at end of file diff --git a/sthome-ut2.yaml b/sthome-ut2.yaml new file mode 100644 index 0000000..86b12d1 --- /dev/null +++ b/sthome-ut2.yaml @@ -0,0 +1,394 @@ +substitutions: + name: sthome-ut2 + friendly_name: "sthome-ut2" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +# - priority: 200 # Network connections like MQTT/native API are set up at this priority. +# then: +# - lambda: |- + +esp32: + board: esp32dev + framework: + type: arduino + #type: esp-idf + +#debug: +# update_interval: 5s + +# Enable logging +logger: + level: DEBUG + +# Enable Home Assistant API +api: + encryption: + key: "o+fgr4qh0rTegCxXE3jbfJ/0si8+W9wxgnnYX9Xucqw=" + +ota: + - platform: esphome + password: "cb8a82d04c2f4dcf9bd273f903c7c378" + +wifi: + #ssid: !secret wifi_ssid + #password: !secret wifi_password + # we will use local dns server for local dns resolution + power_save_mode: none + domain: ".sthome.org" + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, set a static IP address + # Set this to the IP of the ESP + static_ip: 10.0.2.2 + gateway: 10.0.0.2 + subnet: 255.255.240.0 + dns1: 10.0.0.1 + dns2: 1.1.1.1 + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "geuZrJgQo7cg" + +captive_portal: + + +one_wire: + - platform: gpio + pin: GPIO4 + id: temperature_sensors + +#i2c: +# sda: GPIO21 +# scl: GPIO22 + +font: + - file: "fonts/Windows/comic.ttf" + id: my_font + size: 20 + bpp: 2 + + - file: "fonts/misc/tom-thumb.bdf" + id: tomthumb + + # gfonts://family[@weight] + - file: "gfonts://Roboto" + id: roboto_20 + size: 20 + + - file: + type: gfonts + family: Roboto + weight: 900 + id: roboto_16 + size: 16 + + - file: "fonts/Windows/arial.ttf" + id: arial_14 + size: 14 + # gfonts://family[@weight] + - file: "gfonts://Roboto" + id: roboto + size: 20 + - file: "gfonts://Carrois Gothic" + id: Carrois_Gothic + size: 20 + - file: "gfonts://Kdam Thmor Pro" + id: Kdam_Thmor_Pro + size: 40 + - file: "gfonts://Merriweather" + id: Merriweather + size: 30 + + - file: "gfonts://Material+Symbols+Outlined" + id: icons_50 + size: 50 + glyphs: ["\U0000e425"] # mdi-timer + + - file: "fonts/roboto/Roboto-Condensed.ttf" + id: roboto_special_28 + size: 28 + bpp: 4 + glyphs: [ + 0123456789aAáÁeEéÉ, + (,),+,-,_,.,°,•,µ, + "\u0020", # space + "\u002C", # , + "\u0021", # ! + "\u0022", # " + "\u0027", # ' + "\u003A", # : + ] + + - file: "fonts/roboto/Roboto-Condensed.ttf" + id: roboto_special_32 + size: 32 + bpp: 4 + glyphs: [ + 0123456789aAáÁeEéÉ, + (,),+,-,_,.,°,•,µ, + "\u0020", # space + "\u002C", # , + "\u0021", # ! + "\u0022", # " + "\u0027", # ' + "\u003A", # : + ] + + - file: "fonts/roboto/Roboto-Condensed.ttf" + id: roboto_special_36 + size: 36 + bpp: 4 + glyphs: [ + 0123456789aAáÁeEéÉ, + (,),+,-,_,.,°,•,µ, + "\u0020", # space + "\u002C", # , + "\u0021", # ! + "\u0022", # " + "\u0027", # ' + "\u003A", # : + ] + +# - file: "fonts/RobotoCondensed-Regular.ttf" +# id: my_font_with_icons +# size: 20 +# bpp: 4 +# extras: +# - file: "fonts/misc/materialdesignicons-webfont.ttf" +# glyphs: [ +# "\U000F02D1", # mdi-heart +# "\U000F05D4", # mdi-airplane-landing +# ] + + - file: + type: gfonts + family: Roboto + id: roboto_european_core + size: 16 + glyphsets: + - GF_Latin_Core + - GF_Greek_Core + - GF_Cyrillic_Core + + - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: web_font + size: 20 + - file: + url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + type: web + id: web_font2 + size: 24 + + +#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 +psram: + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: +# - 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: +# - lambda: |- +# id(time_synched) = true; +# id(init_holidays).execute(); // we need valid time to calculate holidays +# - logger.log: "Synchronized system clock"# + +display: + - platform: ili9xxx + model: ili9341 + id: tft_display + color_palette: 8BIT + data_rate: 40MHz + #dc_pin: GPIO02 + #reset_pin: GPIO27 + #cs_pin: GPIO15 + reset_pin: GPIO32 + dc_pin: GPIO27 + invert_colors: false + show_test_card: true + dimensions: + width: 480 + height: 320 + +# - platform: ssd1306_i2c +# model: "SSD1306 128x64" +## reset_pin: GPIO32 +# address: 0x3C +# id: oled_display +# pages: +# - id: page1 +# lambda: |- +# auto time_obj = id(time_source).now(); +# auto date = time_obj.strftime("%Y-%m-%d"); +# auto time = time_obj.strftime("%H:%M:%S"); +# it.print(0, 0, id(roboto_16), date.c_str()); +# it.print(0, 20, id(roboto_special_36), time.c_str()); +# - id: page2 +# lambda: |- +# // Print WiFi Signal +# it.printf(0, 26, id(arial_14), "Wi-Fi: %.1fdBm", id(wifi_sig).state); +# // Print time in HH:MM format +# it.strftime(0, 0, id(roboto), TextAlign::TOP_LEFT, "%H:%M", id(time_source).now()); +# //// Print Room humidity"(from xiaomi sensor) +# //if (id(room_humidity).has_state()) { +# // it.printf(127,62, id(roboto), TextAlign::BOTTOM_RIGHT, "%.1f%%", id(room_humidity).state); +# //} +# // Print Study temperature(from dallas sensor) +# if (id(study_temperature).has_state()) { +# it.printf(127, 0, id(Carrois_Gothic), TextAlign::TOP_RIGHT, "%.1f°C", id(study_temperature).state); +# } +# - id: page3 +# lambda: |- +# // Print 29Gal temperature(from dallas sensor) +# if (id(study_temperature).has_state()) { +# it.printf(64, 10, id(Kdam_Thmor_Pro), TextAlign::CENTER_HORIZONTAL, "%.1f°C", id(study_temperature).state); +# } +# - id: page4 +# lambda: |- +# // Print time in HH:MM format +# it.strftime(64, 10, id(Kdam_Thmor_Pro), TextAlign::CENTER_HORIZONTAL, "%H:%M", id(time_source).now()); + +#interval: +#- interval: 5s +# then: +# - display.page.show_next: oled_display +# - component.update: oled_display + +switch: + - platform: restart + name: "${name} Restart" + id: "restart_switch" + +sensor: + - platform: dallas_temp + address: 0xfe00000037b3d528 + name: "Study Temperature" + id: study_temperature + update_interval: "60s" + resolution: 12 + one_wire_id: 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 + id: wifi_sig + 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 + +text_sensor: + - 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 + + # human readable update text sensor from sensor:uptime + - platform: template + name: Uptime + id: uptime_human + icon: mdi:clock-start \ No newline at end of file diff --git a/sthome-ut3.yaml b/sthome-ut3.yaml new file mode 100644 index 0000000..c01c2f5 --- /dev/null +++ b/sthome-ut3.yaml @@ -0,0 +1,339 @@ +packages: + - !include common/wifi.yaml + - !include common/canbus.yaml + +substitutions: + name: sthome-ut3 + friendly_name: "sthome-ut3" + v_ON: "{ 0x4F,0x4E }" + v_OFF: "{ 0x4F, 0x46, 0x46 }" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +globals: + - id: geyser_relay_status + type: bool + restore_value: yes + initial_value: 'false' + +esp32: + board: nodemcu-32s #esp32dev + framework: + type: arduino + +# Example configuration entry +#debug: +# update_interval: 5s + +# Enable logging +logger: + level: verbose + +# Enable Home Assistant API +api: + encryption: + key: "AIoquKPjpcHa2pcJ0aKxvtpM3mwgZuZhpCPtdVitP2Q=" + +ota: + - platform: esphome + password: "879012af7180c8700cee65fbf18704d1" + +wifi: + manual_ip: + static_ip: 10.0.2.3 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "cGXb2DqkwaOr" + +captive_portal: + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: + - platform: homeassistant + id: time_source + update_interval: 360min # Change sync interval from default 5min to 6 hours + on_time_sync: + then: + - if: # Publish the time the device was last restarted, but only once. + condition: + lambda: 'return id(device_last_restart).state == "";' + then: + - text_sensor.template.publish: + id: device_last_restart + state: !lambda 'return id(time_source).now().strftime("%a %d %b %Y - %I:%M:%S %p");' + +interval: + - interval: 30s + then: + - if: + condition: + lambda: 'return !isnan(id(geyser_top_temperature).raw_state);' + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_TOP_TEMPERATURE} + data: !lambda |- + double temp = id(geyser_top_temperature).raw_state; + int value = (int) (temp * 16777216); + std::vector byte_stream(sizeof(value)); + memcpy(byte_stream.data(), &value, sizeof(value)); + ESP_LOGI("SND:${CB_GEYSER_TOP_TEMPERATURE}", "Geyser TOP temp: %d (%f)", value, temp); + //ESP_LOGI("SND:0x400", "Geyser TOP temp: %f, x3: %d, x2: %d, x1: %d, x0: %d, xSize: %d", temp, byte_stream[3],byte_stream[2], byte_stream[1], byte_stream[0], byte_stream.size()); + return byte_stream; + - delay: 1s + - if: + condition: + lambda: 'return !isnan(id(geyser_bottom_temperature).raw_state);' + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_BOTTOM_TEMPERATURE} + data: !lambda |- + double temp = id(geyser_bottom_temperature).raw_state; + int value = (int) (temp * 16777216); + std::vector byte_stream(sizeof(value)); + memcpy(byte_stream.data(), &value, sizeof(value)); + ESP_LOGI("SND:${CB_GEYSER_BOTTOM_TEMPERATURE}", "Geyser BOTTOM temp: %d (%f)", value, temp); + //ESP_LOGI("SND:0x401", "Geyser BOTTOM temp: %f", temp, byte_stream[3],byte_stream[2], byte_stream[1], byte_stream[0], byte_stream.size()); + return byte_stream; + - delay: 1s + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_ENERGISED} + data: !lambda |- + if (id(geyser_heating).state) { + std::vector byte_stream = ${v_ON}; + ESP_LOGI("SND:${CB_GEYSER_ENERGISED}", "Geyser state: %c%c", byte_stream[0], byte_stream[1]); + return byte_stream; + } + else { + std::vector byte_stream = ${v_OFF}; + ESP_LOGI("SND:${CB_GEYSER_ENERGISED}", "Geyser state: %c%c%c", byte_stream[0], byte_stream[1], byte_stream[2]); + return byte_stream; + } + + - interval: 10s + then: + - canbus.send: + canbus_id: canbus_sthome + data: [0x48, 0x45, 0x4C, 0x4C, 0x4F] + - lambda: |- + ESP_LOGI("SND:${CB_CANBUS_ID3}", "HELLO"); + +spi: + - id: spi_bus0 + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + interface: any + +# CAN BUS +canbus: + - platform: mcp2515 + cs_pin: GPIO16 + spi_id: spi_bus0 + mode: NORMAL + id: canbus_sthome + can_id: ${CB_CANBUS_ID3} + bit_rate: 500KBPS #20KBPS + on_frame: +# - can_id: 0 +# can_id_mask: 0 +# then: +# - logger.log: +# format: "ID: 0x%03X -> %s" +# args: [ id, x ] +# tag: "canbus" + - can_id: 0 + can_id_mask: 0 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGI("REC: 0", "%s", &b[0] ); + //ESP_LOGI("canbus", "REC: ID: %d, data: %s, rtr: %s", + // x.can_id, + // x.data.c_str(), + // x.remote_transmission_request ? "true" : "false"); + + +# - can_id: ${CB_CANBUS_ID1} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID1}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID2} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID2}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID3} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID3}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID4} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID4}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID5} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID5}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID6} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID6}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID7} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID7}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID8} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID8}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID9} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID9}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID10} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID10}", "%s", &b[0] ); + +# Define a PWM output on the ESP32 +output: + - platform: ledc + pin: GPIO26 + id: backlight_pwm + +# Define a monochromatic, dimmable light for the backlight +light: + - platform: monochromatic + output: backlight_pwm + name: "Display Backlight" + id: back_light + restore_mode: ALWAYS_ON + +switch: + - platform: restart + name: "${name} Restart" + id: "restart_switch" + +binary_sensor: + - platform: homeassistant + name: "Geyser Heating" + entity_id: binary_sensor.sthome_ut8_heating + id: geyser_heating + on_press: + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_ENERGISED} + data: !lambda |- + std::vector byte_stream = ${v_ON}; + ESP_LOGI("SND:${CB_GEYSER_ENERGISED}", "Geyser state: %c%c", byte_stream[0], byte_stream[1]); + return byte_stream; + on_release: + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_ENERGISED} + data: !lambda |- + std::vector byte_stream = ${v_OFF}; + ESP_LOGI("SND:${CB_GEYSER_ENERGISED}", "Geyser state: %c%c%c", byte_stream[0], byte_stream[1], byte_stream[2]); + return byte_stream; + +sensor: + - platform: homeassistant + name: "Geyser Top Temperature" + entity_id: sensor.sthome_ut8_geyser_top_temperature + id: geyser_top_temperature + on_raw_value: + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_TOP_TEMPERATURE} + data: !lambda |- + double temp = id(geyser_top_temperature).raw_state; + int value = (int) (temp * 16777216); + std::vector byte_stream(sizeof(value)); + memcpy(byte_stream.data(), &value, sizeof(value)); + ESP_LOGI("SND:${CB_GEYSER_TOP_TEMPERATURE}", "Geyser TOP temp: %d (%f)", value, temp); + //ESP_LOGI("SND:0x400", "Geyser TOP temp: %f, x3: %d, x2: %d, x1: %d, x0: %d, xSize: %d", temp, byte_stream[3],byte_stream[2], byte_stream[1], byte_stream[0], byte_stream.size()); + return byte_stream; + + - platform: homeassistant + name: "Geyser Bottom Temperature" + entity_id: sensor.sthome_ut8_geyser_bottom_temperature + id: geyser_bottom_temperature + on_raw_value: + then: + - canbus.send: + canbus_id: canbus_sthome + can_id: ${CB_GEYSER_BOTTOM_TEMPERATURE} + data: !lambda |- + double temp = id(geyser_bottom_temperature).raw_state; + int value = (int) (temp * 16777216); + std::vector byte_stream(sizeof(value)); + memcpy(byte_stream.data(), &value, sizeof(value)); + ESP_LOGI("SND:${CB_GEYSER_BOTTOM_TEMPERATURE}", "Geyser BOTTOM temp: %d (%f)", value, temp); + //ESP_LOGI("SND:0x401", "Geyser BOTTOM temp: %f", temp, byte_stream[3],byte_stream[2], byte_stream[1], byte_stream[0], byte_stream.size()); + return byte_stream; + + # Report wifi signal strength every 5 min if changed + - platform: wifi_signal + name: WiFi Signal + id: wifi_sig + update_interval: 300s + filters: + - delta: 10% + +text_sensor: +# - 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: 60s + + # Expose WiFi information as sensors + - platform: wifi_info + ip_address: + name: IP + update_interval: 10s + mac_address: + name: Mac Address + entity_category: diagnostic + ssid: + name: "Connected SSID" + id: ssid + entity_category: diagnostic + + # human readable update text sensor from sensor:uptime + - platform: template + name: Uptime + id: uptime_human + icon: mdi:clock-start + + - platform: template + name: 'Last Restart' + id: device_last_restart + icon: mdi:clock + entity_category: diagnostic \ No newline at end of file diff --git a/sthome-ut4.yaml b/sthome-ut4.yaml new file mode 100644 index 0000000..ae5f723 --- /dev/null +++ b/sthome-ut4.yaml @@ -0,0 +1,90 @@ +substitutions: + name: sthome-ut4 + friendly_name: "sthome-ut4" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +esp32: + board: esp32-c3-devkitm-1 + variant: ESP32C3 + framework: + type: esp-idf + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "qmfAFWSynz6VhvR5oBrtx2wnNP8n1yZKslVGNDMTWMc=" + +ota: + - platform: esphome + password: "2144396ae4f6a829dc7fe7de88c5887b" + +wifi: + #ssid: !secret wifi_ssid + #password: !secret wifi_password + output_power: 8.5dB + # we will use local dns server for local dns resolution + domain: ".sthome.org" + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, set a static IP address + # Set this to the IP of the ESP + static_ip: 10.0.2.4 + gateway: 10.0.0.2 + subnet: 255.255.240.0 + dns1: 10.0.0.1 + dns2: 10.0.0.2 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "FtGGLAU7OFPx" + +captive_portal: + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: + platform: sntp + +switch: + - platform: gpio + pin: + number: GPIO10 + inverted: true + id: relay5 + name: "Relay 5" + icon: "mdi:lock-outline" + restore_mode: RESTORE_DEFAULT_OFF + #on_turn_on: + # - delay: 1000ms + # - switch.turn_off: relay5 + - platform: gpio + pin: + number: GPIO0 + inverted: true + id: relay6 + name: "Relay 6" + icon: "mdi:gate" + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 500ms + - switch.turn_off: relay6 \ No newline at end of file diff --git a/sthome-ut6.yaml b/sthome-ut6.yaml new file mode 100644 index 0000000..cc3be0a --- /dev/null +++ b/sthome-ut6.yaml @@ -0,0 +1,235 @@ +substitutions: + name: sthome-ut6 + friendly_name: "sthome-ut6" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + on_boot: + - priority: 600 + then: + - lambda: |- + id(restore_remote_lock).execute(); + +globals: + - id: remote_lock_status + type: bool + restore_value: yes + initial_value: 'false' + +esp8266: + board: esp01_1m + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "K3yXQthpXVNknD4RCLZX66gglNgDEFtj3H0r85VLBNs=" + +ota: + - platform: esphome + password: "5956a60f6cf40cf4b6b172e23f236572" + +wifi: + #ssid: !secret wifi_ssid + #password: !secret wifi_password + # we will use local dns server for local dns resolution + domain: ".sthome.org" + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, set a static IP address + # Set this to the IP of the ESP + static_ip: 10.0.2.6 + gateway: 10.0.0.2 + subnet: 255.255.240.0 + dns1: 10.0.0.1 + dns2: 10.0.0.2 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "HIjEc5P2BJhz" + +captive_portal: + +##################################################################################### + +# Minimal flash writes, once per hour +preferences: + flash_write_interval: 60min + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +# Sync time with Home Assistant +time: + - platform: homeassistant + id: homeassistant_time + +# Blink LED for status, and also expose to HA as switch +#light: +# - platform: status_led +# name: "${friendly_name} status light" +# id: blueled +# pin: +# number: GPIO2 +# inverted: yes +# restore_mode: RESTORE_DEFAULT_OFF +# +#script: +# - id: heartbeat +# mode: single +# then: +# - light.toggle: blueled +# - delay: 20 ms +# - light.toggle: blueled +# +# Heartbeat while connected to HA +#interval: +# - interval: 5s +# then: +# if: +# condition: +# api.connected: +# then: +# - script.execute: heartbeat +# + +switch: + # Switch to restart the ESP + - platform: restart + name: ${friendly_name} Restart + + # depending on current state of gate, it will open, close or stop closing / opening + - platform: gpio + pin: GPIO0 + name: "Gate Remote Trigger" + inverted: false + id: gate_remote_trigger + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 1000ms + - switch.turn_off: gate_remote_trigger + + # this is a latching relay, so each pulse toggles the state of the relay + - platform: gpio + pin: GPIO1 + name: "Gate Remote Lock Toggle" + inverted: false + id: gate_remote_lock_toggle + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: +# - lambda: |- +# ESP_LOGD("info", "1: Remote lock status: %d [%d]", id(remote_lock_status), id(gate_remote_lock_sensor).state); + - delay: 200ms + - switch.turn_off: gate_remote_lock_toggle + - delay: 100ms + - lambda: |- + id(remote_lock_status) = id(gate_remote_lock_sensor).state; + // ESP_LOGD("info", "2: Remote lock status: %d [%d]", id(remote_lock_status), id(gate_remote_lock_sensor).state); + +binary_sensor: + - platform: status + # Status platform provides a connectivity sensor + name: "${friendly_name} Status" + device_class: connectivity + + - platform: gpio + pin: + number: GPIO3 + inverted: false + id: gate_remote_lock_sensor + name: Gate Remote Lock Sensor + device_class: lock + +sensor: + # Report wifi signal strength every 5 min if changed + - platform: wifi_signal + name: ${friendly_name} WiFi Signal + update_interval: 300s + filters: + - delta: 10% + # human readable uptime sensor output to the text sensor above + - platform: uptime + name: ${friendly_name} Uptime in Days + id: uptime_sensor_days + update_interval: 60s + 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; + return ( + (days ? String(days) + "d " : "") + + (hours ? String(hours) + "h " : "") + + (minutes ? String(minutes) + "m " : "") + + (String(seconds) + "s") + ).c_str(); + + +text_sensor: + # Expose WiFi information as sensors + - platform: wifi_info + ip_address: + name: ${friendly_name} IP + mac_address: + name: ${friendly_name} Mac Address + + # human readable update text sensor from sensor:uptime + - platform: template + name: Uptime + id: uptime_human + icon: mdi:clock-start + +script: + - id: restore_remote_lock + then: + - lambda: |- + bool restore_value = id(remote_lock_status); + id(set_remote_lock).execute(restore_value); + - delay: 250ms + # make double sure after delay + - lambda: |- + bool restore_value = id(remote_lock_status); + id(set_remote_lock).execute(restore_value); + + - id: set_remote_lock + parameters: + on_state: bool + then: + - lambda: |- + bool lockstate = id(gate_remote_lock_sensor).state; + // next statement is XOR; causes a skip if states are the same + if((on_state && !lockstate) || (!on_state && lockstate)) { + // toggle + id(gate_remote_lock_toggle).turn_on(); + } + +# Pin assignments for ESP-01 +# https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ +# 3v3 | | RX/GPIO3 - high at boot +# RST | | GPIO0 - pulled up, flash if low on boot +# EN | | GPIO2 - pulled up, blue led on if pulled down, must be high at boot +# TX | | GND +# ^ TX/GPIO1 - high at boot \ No newline at end of file diff --git a/sthome-ut7.yaml b/sthome-ut7.yaml new file mode 100644 index 0000000..17a6566 --- /dev/null +++ b/sthome-ut7.yaml @@ -0,0 +1,177 @@ +substitutions: + name: sthome-ut7 + friendly_name: "sthome-ut7" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +#esphome: +# name: sthome-ut7 +# friendly_name: sthome-ut7 + +esp8266: + board: esp01_1m + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "0H1KvuIcSV11klUG1MB8wyalPSNhlk40jkfEzBK95WU=" + +ota: + - platform: esphome + password: "be731abfa319b072b199a257af6e9527" + +wifi: + #ssid: !secret wifi_ssid + #password: !secret wifi_password + # we will use local dns server for local dns resolution + domain: ".sthome.org" + networks: + - ssid: !secret wifi_ssid1 + password: !secret wifi_password1 + - ssid: !secret wifi_ssid2 + password: !secret wifi_password2 + - ssid: !secret wifi_ssid3 + password: !secret wifi_password3 + - ssid: !secret wifi_ssid4 + password: !secret wifi_password4 + - ssid: !secret wifi_ssid5 + password: !secret wifi_password5 + manual_ip: + # For faster connection startup, set a static IP address + # Set this to the IP of the ESP + static_ip: 10.0.2.7 + gateway: 10.0.0.2 + subnet: 255.255.240.0 + dns1: 10.0.0.1 + dns2: 10.0.0.2 + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${friendly_name} Fallback Hotspot" + password: "0JNEPN1Q7GZ4" + +captive_portal: + + +##################################################################################### + +# Minimal flash writes, once per hour +preferences: + flash_write_interval: 60min + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +# Sync time with Home Assistant +time: + - platform: homeassistant + id: homeassistant_time + +switch: + # Switch to restart the ESP + - platform: restart + name: ${friendly_name} Restart + + - platform: gpio + pin: GPIO0 + name: "Relay1" + inverted: false + id: relay1 + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 2500ms + - switch.turn_off: relay1 + + - platform: gpio + pin: GPIO1 + name: "Relay2" + inverted: true + id: relay2 + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 1000ms + - switch.turn_off: relay2 + + - platform: gpio + pin: GPIO2 + name: "Relay3" + inverted: true + id: relay3 + restore_mode: RESTORE_DEFAULT_OFF + on_turn_on: + - delay: 1000ms + - switch.turn_off: relay3 + +binary_sensor: + - platform: status + # Status platform provides a connectivity sensor + name: "${friendly_name} - Status" + device_class: connectivity + + - platform: gpio + pin: + number: GPIO3 + inverted: false + id: gpio3_sensor + name: GPIO3 Sensor + device_class: problem + +sensor: + # Report wifi signal strength every 5 min if changed + - platform: wifi_signal + name: ${friendly_name} WiFi Signal + update_interval: 300s + filters: + - delta: 10% + # human readable uptime sensor output to the text sensor above + - platform: uptime + name: ${friendly_name} Uptime in Days + id: uptime_sensor_days + update_interval: 60s + 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; + return ( + (days ? String(days) + "d " : "") + + (hours ? String(hours) + "h " : "") + + (minutes ? String(minutes) + "m " : "") + + (String(seconds) + "s") + ).c_str(); + + +text_sensor: + # Expose WiFi information as sensors + - platform: wifi_info + ip_address: + name: ${friendly_name} IP + mac_address: + name: ${friendly_name} Mac Address + + # human readable update text sensor from sensor:uptime + - platform: template + name: Uptime + id: uptime_human + icon: mdi:clock-start +# Pin assignments for ESP-01 +# https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ +# 3v3 | | RX/GPIO3 - high at boot +# RST | | GPIO0 - pulled up, flash if low on boot +# EN | | GPIO2 - pulled up, blue led on if pulled down, must be high at boot +# TX | | GND +# ^ TX/GPIO1 - high at boot diff --git a/sthome-ut8.yaml b/sthome-ut8.yaml new file mode 100644 index 0000000..53a6cc5 --- /dev/null +++ b/sthome-ut8.yaml @@ -0,0 +1,2795 @@ +packages: + - !include common/wifi.yaml + - !include common/canbus.yaml + - !include common/geyser.yaml + +substitutions: + name: sthome-ut8 + friendly_name: "sthome-ut8" + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + platformio_options: + build_flags: -fexceptions + build_unflags: -fno-exceptions + includes: + - source # copies folder with files to relevant to be included in esphome compile + - # angle brackets ensure file is included above globals in main.cpp. Make sure to use include GUARDS in the file to prevent double inclusion + - + - + - + - + + + on_boot: + - priority: 600 # This is where most sensors are set up (higher number means higher priority) + then: + - lambda: |- + id(timer_start) = 0; + id(can_msgctr) = 0; + id(time_synched) = false; + id(init_fixed_holidays).execute(); + id(init_schedule).execute(); + +# - priority: 200 # Network connections like MQTT/native API are set up at this priority. +# then: +# - lambda: |- + + +globals: + - 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_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[${HEATING_DAY_TYPES}][${HEATING_DAY_BLOCKS}][3] + restore_value: no # initialised by script + - id: fixed_holidays + type: int[10][2] + restore_value: no # initialised by script + - id: holidays + type: int[12][2] + restore_value: no + - id: is_holiday + type: bool + restore_value: false + - id: energy_counters_reset_time + type: time_t + initial_value: '0' + restore_value: yes +# SOLAR BATTERY / CAN BUS + - id: can_msgctr + type: int + restore_value: no + - id: can_lastid + type: uint32_t + restore_value: no + - id: can_lastframe + type: std::vector + restore_value: no +# - id: g_cb_frames +# type: std::map +# restore_value: no + - id: g_cb_cache + type: solar::cbf_cache + restore_value: no + +esp32: + board: esp32dev + framework: + type: esp-idf + +# Enable logging +logger: + level: 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 + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +time: +# - 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: + - lambda: |- + id(time_synched) = true; + id(init_holidays).execute(); // we need valid time to calculate holidays + // id(show_schedule).execute(); // for debugging + id(can_msgctr) = 0; + + - 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(get_ha_settings).execute(); + // id(update_power_counters).execute(); + id(set_active_schedule).execute(); + id(set_active_heating_timers).execute(); + id(set_geyser_relay).execute(); + id(set_heat_indicators).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"); + +canbus: + - platform: mcp2515 + cs_pin: GPIO15 + spi_id: spi_bus0 + id: canbus_sthome + mode: LISTENONLY + can_id: ${CB_CANBUS_ID8} + bit_rate: 500KBPS + on_frame: + - can_id: 0 + can_id_mask: 0 + then: + - lambda: |- + id(can_msgctr)++; + //if(remote_transmission_request) { + // id(dump_can_message).execute(x, can_id, remote_transmission_request, true, ""); + //} + + - can_id: ${CB_BATTERY_LIMITS} # 0x351 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + if(publish) { + float value = 0.1 * ((x[1] << 8) + x[0]); // unit = 0.1V + id(battery_charge_voltage_limit).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A + id(battery_charge_current_limit).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1A + id(battery_discharge_current_limit).publish_state(value); + } + } + - can_id: ${CB_BATTERY_STATE} # 0x355 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + if(publish) { + auto value = static_cast((x[1] << 8) + x[0]); + id(battery_soc).publish_state(value); + value = static_cast((x[3] << 8) + x[2]); + id(battery_soh).publish_state(value); + } + } + - can_id: ${CB_BATTERY_STATUS} # 0x356 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + if(publish) { + float value = 0.01 * static_cast((x[1] << 8) + x[0]); // unit = 0.01V Voltage of single module or average module voltage of system + id(battery_system_voltage).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A Module or system total current + id(battery_system_current).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1°C + id(battery_average_cell_temperature).publish_state(value); + } + } + - can_id: ${CB_BATTERY_FAULT} # 0x359 + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + 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); + } + } + - 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 pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + 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."); + } + } + } + - can_id: ${CB_BATTERY_MANUFACTURER} # 0x35E + then: + - lambda: |- + using namespace solar; + auto time_obj = id(time_source).now(); + if(time_obj.is_valid()) { + auto pylon_item = cbf_store_pylon(id(can_msgctr), can_id, x, remote_transmission_request, time_obj.timestamp); + bool publish = id(g_cb_cache).additem(pylon_item); + if(publish) { + std::string str(x.begin(), x.end()); + id(battery_manufacturer).publish_state(str); + } + } + +switch: + - platform: restart + name: "${name} Restart" + id: "restart_switch" + + - platform: gpio + pin: + number: GPIO1 + inverted: true + mode: + input: true + pullup: true + id: reset_energy_counters + 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: GPIO12 + inverted: true + mode: + input: true + pullup: true + id: vacation_mode_switch + name: "Vacation Mode" + restore_mode: RESTORE_DEFAULT_OFF + + - platform: gpio + pin: + number: GPIO35 + inverted: true + mode: + input: true + pullup: false # external pullup + id: school_holiday_mode_switch + name: "School Holiday Mode" + restore_mode: RESTORE_DEFAULT_OFF + + - 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", "************* 1: Geyser Relay turned on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "************* 2: Geyser Relay turned off"); + + - platform: gpio + pin: + number: GPIO17 + inverted: false + 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", "************* 1: Pool Relay turned on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "************* 2: Pool Relay turned off"); + +output: + - platform: ledc + pin: + number: GPIO26 + inverted: false + id: led_geyser_temp_blue + - platform: ledc + pin: + number: GPIO25 + inverted: false + id: led_geyser_temp_green + - platform: ledc + pin: + number: GPIO33 + inverted: false + id: led_geyser_temp_yellow + - platform: ledc + pin: + number: GPIO32 + inverted: false + id: led_geyser_temp_red + - platform: ledc + pin: + number: GPIO27 + inverted: false + id: led_holiday + +light: + - platform: monochromatic + output: led_geyser_temp_blue + id: led_geyser_temp1 + on_turn_on: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 1 on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 1 off"); + - platform: monochromatic + output: led_geyser_temp_green + id: led_geyser_temp2 + on_turn_on: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 2 on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 2 off"); + - platform: monochromatic + output: led_geyser_temp_yellow + id: led_geyser_temp3 + on_turn_on: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 3 on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 3 off"); + - platform: monochromatic + output: led_geyser_temp_red + id: led_geyser_temp4 + on_turn_on: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 4 on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "Geyser Temperature LED 4 off"); + - platform: monochromatic + output: led_holiday + name: "LED Holiday" + on_turn_on: + - lambda: |- + ESP_LOGI("info", "Holiday on"); + on_turn_off: + - lambda: |- + ESP_LOGI("info", "Holiday off"); + +binary_sensor: + - platform: status + # Status platform provides a connectivity sensor + name: "Status" + device_class: connectivity + + - platform: gpio + pin: + number: GPIO36 + mode: + input: true + pullup: false # external pullup + filters: + - delayed_off: 50ms + id: inverter_1_battery + name: "Inverter 1 Battery" + device_class: battery + + - platform: gpio + pin: + number: GPIO39 + mode: + input: true + pullup: false # external pullup + filters: + - delayed_off: 50ms + id: inverter_2_battery + name: "Inverter 2 Battery" + device_class: battery + + - platform: template + id: heating + name: "Heating" + lambda: |- + return id(geyser_current).state > 10; + device_class: heat + + - platform: template + id: mains_supply + name: "Mains Supply" + lambda: |- + return id(mains_voltage).state > 200; // minimum acceptable voltage + 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. Turning geyser off."); + # - switch.turn_off: geyser_relay + on_release : + then: + - lambda: |- + ESP_LOGI("info", "Overload is cleared."); + + # in vacation mode, geyser is only switched on when it can be powered by solar only, i.e. without using mains + - platform: template #gpio + id: vacation_mode + #pin: + # number: GPIO04 + # mode: + # input: true + # pullup: true + #filters: + # - delayed_off: 100ms + name: "Vacation Mode" + icon: "mdi:beach" + # remove lambda if controlled by external switch + lambda: |- + return id(vacation_mode_switch).state; + +# 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;" + +sensor: + # 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: 4.096 + ads1115_id: ads1115_48 + sample_rate: 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: 5ms + filters: + # - offset: 0.0002 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 #5000 + send_every: 208 #416 + send_first_at: 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: 4.096 + ads1115_id: ads1115_48 + sample_rate: 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: 5ms + filters: + # - offset: 0.0002 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 #5000 + send_every: 208 #416 + send_first_at: 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: 4.096 + ads1115_id: ads1115_49 + sample_rate: 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: 5ms + filters: + # - offset: 0.0002 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 #5000 + send_every: 208 #416 + send_first_at: 208 #416 + - lambda: return sqrt(x); + - multiply: 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", "Geyser lost power."); + - above: 0.5 + then: + - lambda: |- + ESP_LOGI("info", "Geyser was energised."); + # mod end ####################### + + - platform: ads1115 + multiplexer: A2_A3 + gain: 4.096 + ads1115_id: ads1115_49 + sample_rate: 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: 5ms + filters: + # - offset: 0.0002 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 #5000 + send_every: 208 #416 + send_first_at: 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: 860 + name: "Mains Voltage ADC" + id: mains_voltage_adc + unit_of_measurement: "V" + accuracy_decimals: 8 + icon: "mdi:flash" + multiplexer: A0_A1 + gain: 4.096 + update_interval: 5ms #23ms + device_class: voltage + state_class: measurement + filters: + - offset: 0.0065 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 + send_every: 104 + send_first_at: 104 #416 + - lambda: return sqrt(x); + - multiply: 650 + - lambda: |- + if(abs(x) < 20) + return 0; + return x; + + # ads1115_4A + # Inverter voltage sensor + - platform: ads1115 + ads1115_id: ads1115_4A + sample_rate: 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: 4.096 + update_interval: 5ms #23ms + device_class: voltage + state_class: measurement + filters: + - offset: 0.0131 + - lambda: return x * x; + - sliding_window_moving_average: + window_size: 1250 + send_every: 104 + send_first_at: 104 + - lambda: return sqrt(x); + - multiply: 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: "Calibrate 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: "Calibrate 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: "Calibrate 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: "Calibrate 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: "Calibrate 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: "Calibrate 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' + 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' + 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' + 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' + 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' + 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" + 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" + 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_soc + name: "Battery SOC" + accuracy_decimals: 0 + unit_of_measurement: "%" + state_class: measurement + device_class: battery + - platform: template + id: battery_soh + name: "Battery SOH" + 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 + - 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 + - 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 + +text_sensor: + - 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" + +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 { + id(is_holiday_date).execute(day_seconds); + bool isHoliday = id(is_holiday); + int g_schedule_idx = (isHoliday) ? 3 : (dayofweek == 1) ? 0 : (dayofweek == 7) ? 2 : 1; + 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 { + //ESP_LOGI("info", "1. isholiday: %d : %d, day_schedule: [%d]%d %d / %d / %d", isHoliday, future_endtime, g_schedule_idx, blk, day_schedule[blk][0], day_schedule[blk][1], day_schedule[blk][2]); + 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; + //ESP_LOGI("info", "2. future_endtime: %d, day_schedule: [%d]%d %d / %d / %d", future_endtime, active_idx, active_blk, day_schedule[active_blk][0], day_schedule[active_blk][1], day_schedule[active_blk][2]); + } + } + } + } + while(++blk < ${HEATING_DAY_BLOCKS}); // second dimension of the g_schedule array + dayofweek = (dayofweek < 7) ? dayofweek++ : 1; + day_seconds += 86400; // next day + } + while(++d < ${HEATING_DAY_TYPES}); // first dimension of the g_schedule array + auto day_schedule = id(g_schedule)[active_idx]; + id(active_schedule_temperature) = static_cast(day_schedule[active_blk][0]) / ${HEATING_TEMP_SCALE}; + id(active_schedule_period)[0] = day_schedule[active_blk][1] + today_seconds; + id(active_schedule_period)[1] = day_schedule[active_blk][2] + today_seconds; + //ESP_LOGI("info", "3. day:%d, block:%d, schedule: %d / %d / %d (%d)", active_idx, active_blk, day_schedule[active_blk][0], day_schedule[active_blk][1], day_schedule[active_blk][2], today_seconds); + //for(int i = 0; i < 12; i++) { + // ESP_LOGI("info", "holiday: {%d, %d}", id(holidays)[i][0], id(holidays)[i][1]); + //} + + - id: 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_red = 0; + float led_on = 0.75; + float brightness = 0; + if(temp_bottom < 40) { + brightness = 0.1*(40 - temp_bottom); + led_blue = brightness > 1 ? 1 : brightness; // blue + if(temp_top >= 60) { + brightness = 0.1*(temp_top - 60); + led_red = brightness > 1 ? 1 : brightness; // red + led_yellow = 1; + led_green = 1; + } else if(temp_top >= 50) { + led_yellow = 0.1*(temp_top - 50); // yellow + led_green = 1; + } else if(temp_top >= 40) { + led_green = 0.1*(temp_top - 40); // green + } + } + else if(temp_bottom >= 40 && temp_bottom < 50) { + led_green = 0.1*(50 - temp_bottom); // green + if(temp_top >= 60) { + brightness = 0.1*(temp_top - 60); + led_red = brightness > 1 ? 1 : brightness; // red + led_yellow = 1; + } else if(temp_top >= 50) { + led_yellow = 0.1*(temp_top - 50); // yellow + } + } + else if(temp_bottom >= 50 && temp_bottom < 60) { + led_yellow = 0.1*(60 - temp_bottom); // yellow + if(temp_top >= 60) { + brightness = 0.1*(temp_top - 60); + led_red = brightness > 1 ? 1 : brightness; // red + } + } + else if(temp_bottom > 60) { + 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 - 60); // red + led_red = brightness > 1 ? 1 : brightness; // red + } + if(temp_top >= 60) { + 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 - 60); // red + led_red = brightness > 1 ? 1 : brightness; // red + } + led_blue *= led_on; + led_green *= led_on; + led_yellow *= 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_red).set_level(led_red); + //ESP_LOGI("info", "top: %f, bot: %f, Brightness: led_blue %f, led_green: %f, led_yellow, %f, led_red %f", temp_top, temp_bottom, led_blue, led_green, led_yellow, led_red); + + - id: set_geyser_relay + then: + - lambda: |- + bool inverter_battery_low = id(inverter_1_battery).state || id(inverter_2_battery).state; + //ESP_LOGI("info", "----------- inverter_1_battery %d, inverter_2_battery %d", id(inverter_1_battery).state, id(inverter_2_battery).state); + 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_LOGI("info", "Geyser heating is turned %s.", (relay_on) ? "on" : "off"); + //ESP_LOGI("info", "----------- Inverter battery: %d", inverter_battery); + if(relay_on) { + // GEYSER IS ENERGISED + // =================== + 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_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(inverter_battery_low && !id(mains_supply)) { + // 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) && !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 vacation mode requires it to be switched off + id(vacation_mode_set_geyser_relay).execute(relay_on, sun_high_enough); + } + if(!id(geyser_relay_status)) { + id(geyser_relay).turn_off(); + ESP_LOGI("info", "----------- Geyser was turned off at %f °C.", id(geyser_top_temperature).state); + } + } + else { + // GEYSER IS NOT ENERGISED + // ======================= + 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("info", "+++++++++++ 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(inverter_battery_low && !id(mains_supply)) { + // inverter battery is low + ESP_LOGI("info", "+++++++++++ 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("info", "+++++++++++ 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 vacation mode requires it to be switched off + id(vacation_mode_set_geyser_relay).execute(relay_on, sun_high_enough); + } + if(id(geyser_relay_status)) { + id(geyser_relay).turn_on(); + ESP_LOGI("info", "+++++++++++ 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 vacation mode requires it + - id: vacation_mode_set_geyser_relay + parameters: + relay_on: bool + sun_high_enough: bool + then: + - lambda: |- + if(id(vacation_mode).state) { + // only set/reset geyser_relay_status here if vacation 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_LOGI("info", "+++++++++++ Vacation 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_LOGI("info", "+++++++++++ Vacation mode: not enough solar energy, geyser turned to be off at %f °C. Heating start: %s, end: %s, time: %d, solar: %f kW", id(geyser_top_temperature).state, ESPTime::from_epoch_local(id(active_heating_start)).strftime("%Y-%m-%d %H:%M:%S").c_str(), ESPTime::from_epoch_local(id(active_heating_end)).strftime("%Y-%m-%d %H:%M:%S").c_str(), id(active_heating_time), solar_power); + } + } + else { + // GEYSER IS NOT ENERGISED + // ======================= + if(sun_high_enough) { + id(geyser_relay_status) = true; + } + } + } + + # calculates effective geyser temp, taking into account both bottom and top geyser temperatures + - id: calc_geyser_heating_values + parameters: + temperature_target: double + then: + - lambda: |- + // estimate expected heat loss + double heat_loss = id(thermal_transmittance) * id(geyser_surface_area) * (id(geyser_top_temperature).state - id(ambient_temperature).state); // in Watts + double heating_power = id(geyser_element_power).state - heat_loss; + id(g_heat_loss) = heat_loss; + id(geyser_effective_power) = (heating_power > 0.0001) ? heating_power : 0.0001; // this is to avoid dividing by zero + // use specific_heat_capacity = 4184 J/kg°C to calculate heating factor, i.e. the number of seconds it will take to heat water by 1 degree + double heating_factor = id(watermass) * 4184 / id(geyser_effective_power); + // set estimated heat required + double geyser_temp_diff = id(geyser_top_temperature).state - id(geyser_bottom_temperature).state - id(geyser_top_bottom_constraint); + double geyser_effective_temperature = (geyser_temp_diff > 0) ? id(geyser_top_temperature).state - geyser_temp_diff : id(geyser_top_temperature).state; + double temperature_diff = temperature_target - geyser_effective_temperature; + // set estimated heating time + double heating_time = heating_factor * temperature_diff; // in Joules + id(estimated_heating_time) = static_cast(heating_time); + double overshoot_period = heating_factor * id(temp_overshoot_allowed); + id(estimated_overshoot_time) = static_cast(overshoot_period + heating_time); + + - id: record_heat_gained + then: + - lambda: |- + //ESP_LOGI("info", "Recording heat lost/gained."); + auto currenttime = id(time_source).now(); + id(heat_monitor_end) = currenttime.timestamp; + time_t start_time = id(heat_monitor_start); + time_t time_elapsed = id(heat_monitor_end) - start_time; + if(time_elapsed > 0) { + // heat gained measurement + if(start_time > 0) { + double water_temp = id(geyser_top_temperature).state; + double ambient_temp = id(ambient_temperature).state; + double previous_temp = id(last_geyser_top_temperature); + if(isnan(water_temp)) { + ESP_LOGW("warning", "Geyser top temperature is NaN. Skipping heat gain measurement."); + } + else if (previous_temp < -280.0) { + ESP_LOGW("warning", "Geyser previous top temperature (%.2f) is invalid. Restarting heat gain measurement.", previous_temp); + id(start_heat_monitor).execute(water_temp, ambient_temp); + } + else { + double dtemp = water_temp - previous_temp; + double heat_energy_gained = id(watermass) * 4184 * dtemp; // joules + double heat_gain = heat_energy_gained / time_elapsed; // watts + id(g_heat_gained) = heat_gain; + ESP_LOGI("info", "Geyser temperature loss/gain: %.2f°C, time elapsed %d, heat energy gained: %.0fJ, heat gain: %.0fW", dtemp, time_elapsed, heat_energy_gained, heat_gain); + id(start_heat_monitor).execute(water_temp, ambient_temp); + } + } + } + + - id: start_heat_monitor + parameters: + water_temp: double + ambient_temp: double + then: + - lambda: |- + //ESP_LOGI("info", "Starting heat loss/gain measurement. A: %.2f, T: %.2f", ambient_temp, water_temp); + auto currenttime = id(time_source).now(); + id(heat_monitor_start) = currenttime.timestamp; + if(isnan(water_temp)) { + ESP_LOGW("warning", "Geyser top temperature is NaN. Setting last_geyser_top_temperature to default."); + id(last_geyser_top_temperature) = -301; + } + else { + id(last_geyser_top_temperature) = water_temp; + if(isnan(ambient_temp)) { + ESP_LOGW("warning", "Ambient temperature is NaN. Setting last_temp_diff to default."); + id(last_temp_diff) = -301; + } + else { + id(last_temp_diff) = water_temp - ambient_temp; + } + //ESP_LOGI("info", "Start monitor @ Geyser top temperature: %.2f°C, geyser vs outside: %.2f°C", id(last_geyser_top_temperature), id(last_temp_diff)); + } + + - id: init_fixed_holidays + then: + - lambda: |- + id(fixed_holidays)[0][0] = 1; // New Year's Day + id(fixed_holidays)[0][1] = 1; + id(fixed_holidays)[1][0] = 3; // Human Rights Day + id(fixed_holidays)[1][1] = 21; + id(fixed_holidays)[2][0] = 4; // Freedom Day + id(fixed_holidays)[2][1] = 27; + id(fixed_holidays)[3][0] = 5; // Workers Day + id(fixed_holidays)[3][1] = 1; + id(fixed_holidays)[4][0] = 6; // Youth Day + id(fixed_holidays)[4][1] = 16; + id(fixed_holidays)[5][0] = 8; // Womens Day + id(fixed_holidays)[5][1] = 9; + id(fixed_holidays)[6][0] = 9; // Heritage Day + id(fixed_holidays)[6][1] = 24; + id(fixed_holidays)[7][0] = 12; // Reconciliation Day + id(fixed_holidays)[7][1] = 16; + id(fixed_holidays)[8][0] = 12; // Christmas Day + id(fixed_holidays)[8][1] = 25; + id(fixed_holidays)[9][0] = 12; // Boxing Day + id(fixed_holidays)[9][1] = 26; + + - id: init_schedule + then: + - lambda: |- + // SUNDAYS + int i = 0; + 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, 11); // LATE MORNING + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, 11, 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 = 1; + 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, 11); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, 11, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 28.5); + // SATURDAYS + i = 2; + 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, 11); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, 11, 16); + id(set_schedule_block).execute(i, j++, ${HEATING_IDLE}, 16, 31); // (31 = 7AM next day) + // HOLIDAYS + i = 3; + 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, 11); + id(set_schedule_block).execute(i, j++, ${HEATING_HOT}, 11, 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 > ${HEATING_DAY_TYPES}) { + ESP_LOGW("Set Schedule", "day index of %d is out of bounds. Allowed values: 0 to %d.", day_idx, ${HEATING_DAY_TYPES}); + return; + } + if(block_idx < 0 || block_idx > ${HEATING_DAY_BLOCKS}) { + ESP_LOGW("Set Schedule", "block index of %d is out of bounds. Allowed values: 0 to %d.", block_idx, ${HEATING_DAY_BLOCKS}); + return; + } + id(g_schedule)[day_idx][block_idx][0] = static_cast(temperature * ${HEATING_TEMP_SCALE}); + id(g_schedule)[day_idx][block_idx][1] = static_cast(start_time * 3600); + id(g_schedule)[day_idx][block_idx][2] = static_cast(end_time * 3600); + // ESP_LOGI("SCHEDULE", "// %.2f, %d, (%.2f), %d (%.2f)", temperature, id(g_schedule)[day_idx][block_idx][1], start_time, id(g_schedule)[day_idx][block_idx][2], end_time); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][0] = %d;", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][0]); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][1] = %d;", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][1]); + // ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][2] = %d;\n", day_idx, block_idx, id(g_schedule)[day_idx][block_idx][2]); + + - id: show_schedule + then: + - lambda: |- + for(int d = 0; d < ${HEATING_DAY_TYPES}; d++) { + for(int b = 0; b < ${HEATING_DAY_BLOCKS}; b++) { + int t = id(g_schedule)[d][b][0]; + int s = id(g_schedule)[d][b][1]; + int e = id(g_schedule)[d][b][2]; + float temp = static_cast(t) / ${HEATING_TEMP_SCALE}; + float start_time = static_cast(s) / 3600; + float end_time = static_cast(e) / 3600; + ESP_LOGI("SCHEDULE", "// %.1f°C, %d, (%.2f), %d (%.2f)", temp, s, start_time, e, end_time); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][0] = %d;", d, b, t); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][1] = %d;", d, b, s); + ESP_LOGI("SCHEDULE", "id(g_schedule)[%d][%d][2] = %d;\n", d, b, e); + } + } + + - id: init_holidays + then: + - lambda: |- + // calculate easter first + #include + auto today = id(time_source).now(); + today.second = 0; + today.minute = 0; + today.hour = 0; + auto year = today.year; + auto datevalue = fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)+fmod(32+2*fmod(trunc(year/100),4)+2*trunc(fmod(year,100)/4)-fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)-fmod(year,4),7)-7*trunc((fmod(year,19)+11*fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)+22*fmod(32+2*fmod(trunc(year/100),4)+2*trunc(fmod(year,100)/4)-fmod(19*fmod(year,19)+trunc(year/100)-trunc(year/400)-trunc((trunc(year/100)-trunc((8+year/100)/25)+1)/3)+15,30)-fmod(year,4),7))/451)+114; + today.month = trunc(datevalue/31); + today.day_of_month = 1+fmod(datevalue,31); + today.recalc_timestamp_local(); + auto time_obj = ESPTime::from_epoch_local(today.timestamp - 2*86400); // Good Friday + int i = 0; + id(holidays)[i][0] = time_obj.month; + id(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(holidays)[i][0] = time_obj.month; + id(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_holidays array index + while(j < 10) { + ++i; + time_obj.year = year; + time_obj.month = id(fixed_holidays)[j][0]; + time_obj.day_of_month = id(fixed_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(holidays)[i][0] = holiday.month; + id(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: is_holiday_date + parameters: + date: uint64_t + then: + lambda: |- + auto time_obj = ESPTime::from_epoch_local(date); + int month = time_obj.month; + int day_of_month = time_obj.day_of_month; + int i = 0; + while(i < 12 && (id(holidays)[i][0] != month || id(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]); + id(is_holiday) = (i < 12); + +# Geyser HEATING Calculations +# HEAT LOSS +# The primary formula for calculating heat loss in a water heater is Q = U x A x ΔT, where: +# Q: is the heat loss (in Watts, BTU/hr, etc.) +# U: is the U-value (thermal transmittance) of the heater's insulation (in W/m²°C or BTU/hr ft²°F). +# A: is the surface area of the water heater (in m² or ft²). +# ΔT: is the temperature difference between the water inside the heater and the ambient temperature outside (in °C or °F). +# +# HEAT REQUIRED +# Heat Required in Joules (Q) = m * ΔT * c where: +# m = mass of water in kg +# ΔT = temperature difference in °C +# c = specific heat capacity (water = 4184 J/kg°C). +# +# HEATING TIME +# Heating Time in seconds (t) = Q / W where: +# Q = heat required in Joules +# W = power in Watts of heating element +# +############################################################################################################### +# Alternative ADS1115 sensor +#sensor: + #ads1115_48 +# Sensor will convert ADC output to Current without need for ct_clamp platform sensor +# - platform: ads1115 +# multiplexer: 'A0_A1' +# gain: 1.024 +# name: "Geyser Element Current" +# ads1115_id: ads1115_48 +# update_interval: 0ms #24ms +# id: geyser_element_current_real +# state_class: measurement +# device_class: current +# unit_of_measurement: "A" +# icon: "mdi:flash" +# accuracy_decimals: 8 +# filters: +# # Calculates RMS voltage sampled by the ADS1115 ADC +# - lambda: return x * x; #### +# - sliding_window_moving_average: # +# window_size: 2500 # averages over 2500 update intervals +# send_every: 1250 # reports every 1250 update intervals +# - lambda: return sqrt(x); #### +# - multiply: 88.2 # Map measured voltage from CT clamp to current in the primary circuit + +# CT CLAMP calculations +# Burden Resistor (ohms) = (VREF * CT TURNS) / (√2 * max primary current) +# Primary Current (A) = secondary voltage * CT TURNS / (√2 * burden resistor) +# CT TURNS = primary current * burden resistor / secondary voltage +# Multiplier = CT TURNS / burden resistor (other surrounding circuitry impacts this value) + +# for use with latching relay +# - platform: template +# id: geyser_relay_failures +# name: "Geyser Relay Failures" +# icon: mdi:flash +# accuracy_decimals: 0 +# unit_of_measurement: "" +# lambda: |- +# auto failcount = id(geyser_relay_fail_count); +# if(failcount > 0) { +# id(geyser_relay_fail).turn_on(); +# } +# else { +# id(geyser_relay_fail).turn_off(); +# } +# return failcount; +# update_interval: 10s +# + +# for Benchwork +# - platform: template +# name: "Geyser Top Temperature" +# id: geyser_top_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 60.5114; +# +# - platform: template +# name: "Geyser Bottom Temperature" +# id: geyser_bottom_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 31.2455; +# +# - platform: template +# name: "Ambient Temperature" +# id: ambient_temperature +# update_interval: "30s" +# unit_of_measurement: "°C" +# icon: "mdi:water-thermometer" +# device_class: "temperature" +# state_class: "measurement" +# accuracy_decimals: 1 +# lambda: |- +# return 20.1234; +# end of for Benchwork +# +# script: +# - id: update_power_counters +# then: +# - lambda: |- +# if(id(time_synched)) { +# // power counters +# auto time_obj = id(time_source).now(); +# time_obj.recalc_timestamp_local(); +# int day_of_week = time_obj.day_of_week; +# time_t end_time = static_cast(time_obj.timestamp); +# time_t start_time = static_cast(id(timer_start)); +# double time_elapsed = static_cast(end_time - start_time); +# id(validate_energy_values).execute(day_of_week); +# if(start_time > 0) { +# id(do_power_counters_update).execute(day_of_week, time_elapsed); +# } +# id(timer_start) = end_time; +# } +# +# - id: init_daily_power_counters +# then: +# - lambda: |- +# auto currenttime = id(time_source).now(); +# int day_of_week = currenttime.day_of_week; +# id(geyser_energy_daily)[day_of_week-1] = 0.0; // reset +# id(power_outlets_energy_daily)[day_of_week-1] = 0.0; // reset +# id(mains_energy_daily)[day_of_week-1] = 0.0; // reset +# id(generated_energy_daily)[day_of_week-1] = 0.0; // reset +# id(energy_loss_daily)[day_of_week-1] = 0.0; // reset +# +# - id: init_monthly_power_counters +# then: +# - lambda: |- +# //auto currenttime = id(time_source).now(); +# //int day_of_week = currenttime.day_of_week; +# id(geyser_energy) = 0.0; // reset +# id(power_outlets_energy) = 0.0; // reset +# id(mains_energy) = 0.0; // reset +# id(generated_energy) = 0.0; // reset +# id(energy_loss) = 0.0; // reset +# +# - id: do_power_counters_update +# parameters: +# day_of_week: int +# time_elapsed: double +# then: +# - lambda: |- +# double power = id(geyser_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Geyser power is NaN. Skipping geyser power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(geyser_energy_daily)[day_of_week-1] += energy; +# id(geyser_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(power_outlets_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Plugs Power is NaN. Skipping Plugs Power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(power_outlets_energy_daily)[day_of_week-1] += energy; +# id(power_outlets_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(mains_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Mains power is NaN. Skipping mains power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(mains_energy_daily)[day_of_week-1] += energy; +# id(mains_energy) += energy; +# } +# power = id(lights_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Lights power is NaN. Skipping lights power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(lights_energy_daily)[day_of_week-1] += energy; +# id(lights_energy) += energy; +# id(house_energy_usage) += energy; +# } +# power = id(generated_power).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Generated power is NaN. Skipping generated power counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(generated_energy_daily)[day_of_week-1] += energy; +# id(generated_energy) += energy; +# //ESP_LOGI("info", "Generated energy: %f kWs. Total: %f kWh", energy, id(generated_energy)/3600.0); +# } +# power = id(power_loss).state; +# if(isnan(power)) { +# ESP_LOGW("warning", "Energy loss is NaN. Skipping energy loss counters update."); +# } +# else { +# double energy = time_elapsed * power; +# id(energy_loss_daily)[day_of_week-1] += energy; +# id(energy_loss) += energy; +# } +# - id: validate_energy_values +# parameters: +# day_of_week: int +# then: +# - lambda: |- +# if(isnan(id(geyser_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Geyser Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(geyser_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(power_outlets_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Plugs Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(power_outlets_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(mains_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Mains Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(mains_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(lights_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Lights Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(lights_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(generated_energy_daily)[day_of_week-1])) { +# ESP_LOGW("warning", "Generated Energy Usage for day %d is NaN. Value was reset to zero.", day_of_week); +# id(generated_energy_daily)[day_of_week-1] = 0; +# } +# if(isnan(id(geyser_energy))) { +# ESP_LOGW("warning", "Geyser Energy is NaN. Value was reset to zero."); +# id(geyser_energy) = 0; +# } +# if(isnan(id(power_outlets_energy))) { +# ESP_LOGW("warning", "Plugs Energy is NaN. Value was reset to zero."); +# id(power_outlets_energy) = 0; +# } +# if(isnan(id(mains_energy))) { +# ESP_LOGW("warning", "Mains Energy is NaN. Value was reset to zero."); +# id(mains_energy) = 0; +# } +# if(isnan(id(lights_energy))) { +# ESP_LOGW("warning", "Lights Energy is NaN. Value was reset to zero."); +# id(lights_energy) = 0; +# } +# if(isnan(id(generated_energy))) { +# ESP_LOGW("warning", "Generated Energy is NaN. Value was reset to zero."); +# id(generated_energy) = 0; +# } \ No newline at end of file diff --git a/sthome-ut9.yaml b/sthome-ut9.yaml new file mode 100644 index 0000000..848d7b4 --- /dev/null +++ b/sthome-ut9.yaml @@ -0,0 +1,1809 @@ +packages: + - !include common/wifi.yaml + - !include common/canbus.yaml + +substitutions: + name: sthome-ut9 + friendly_name: "sthome-ut9" + #ALLOWED_CHARACTERS_FULL: " !#%\"'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ¿ÁÂÄÅÉÖÚßàáâãäåæçèéêëìíîðñòóôõöøùúûüýþāăąćčďĐđēėęěğĮįıļľŁłńňőřśšťũūůűųźŻżŽžơưșțΆΈΌΐΑΒΓΔΕΖΗΘΚΜΝΠΡΣΤΥΦάέήίαβγδεζηθικλμνξοπρςστυφχψωϊόύώАБВГДЕЖЗИКЛМНОПРСТУХЦЧШЪЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёђєіїјљњћ" + ALLOWED_CHARACTERS: " !#%\"'()+,-./0123456789:;<>?@ABCDEFGHIJKLMNOPQRSTUVWYZ[]_abcdefghijklmnopqrstuvwxyz{|}°²³µ•" + DD_MAX_YEARS: "5" + +globals: + - id: g_month_idx + type: int + restore_value: yes + initial_value: '0' + - id: g_year_idx + type: int + restore_value: yes + initial_value: '0' + - id: g_options_year + type: char[1 + ${DD_MAX_YEARS} * 5] + restore_value: yes +# initial_value: "{2022\n2023\n2024\n2025\n2026}" + - id: g_geyser_heating_on + type: bool + restore_value: no + initial_value: '0' + - id: g_utility_on + type: bool + restore_value: no + initial_value: '0' + - id: g_geyser_top_temperature + type: double + restore_value: yes + initial_value: '0' + - id: g_geyser_bottom_temperature + type: double + restore_value: yes + initial_value: '0' + - id: can_lastid + type: uint32_t + restore_value: no + - id: can_lastframe + type: std::vector + restore_value: no + +esphome: + name: "${name}" + friendly_name: "${friendly_name}" + +esp32: +# board: nodemcu-32s + board: esp32dev + framework: + type: arduino + #type: esp-idf + +#debug: +# update_interval: 5s + +# Enable logging +logger: + level: DEBUG + logs: + canbus: INFO + +# Enable Home Assistant API +api: + encryption: + key: "LI7j37zs9HsWNsUZ5c83leThmhHsgIVReAPoc9U6pVU=" + +ota: + - platform: esphome + password: "8ebd5bcefbdc833a5f6ddc4e8ba56e39" + +wifi: + power_save_mode: none # stops display flickering + manual_ip: + static_ip: 10.0.2.9 + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "${name} Fallback Hotspot" + password: "iZxjpw7ucRs4" + +captive_portal: + +time: + - platform: homeassistant + id: time_source + update_interval: 360min # Change sync interval from default 5min to 6 hours + on_time_sync: + then: +# - if: # Publish the time the device was last restarted, but only once. +# condition: +# lambda: 'return id(device_last_restart).state == "";' +# then: +# - text_sensor.template.publish: +# id: device_last_restart +# state: !lambda 'return id(time_source).now().strftime("%a %d %b %Y - %I:%M:%S %p");' +# - script.execute: ind_heating_update +# - script.execute: time_update +# - script.execute: init_calendar + + on_time: + - minutes: '*' + seconds: '*' + then: +# - script.execute: ind_heating_update +# - script.execute: time_update +# - lambda: |- +# id(get_calendar_days_state).execute("T"); + #- script.execute: get_calendar_days_state +# +# - hours: 1,2,3,4 +# minutes: 5 +# seconds: 0 +# then: +# - switch.turn_on: switch_antiburn +# - hours: 1,2,3,4 +# minutes: 35 +# seconds: 0 +# then: +# - switch.turn_off: switch_antiburn + +font: + - file: "gfonts://Roboto" + id: roboto_200 + size: 200 + bpp: 4 + glyphs: [ + 0123456789,.,°,a,n, + "\u0020", # space + "\u003A", # colon + ] + - file: "gfonts://Roboto" + id: roboto_192 + size: 192 + bpp: 4 + glyphs: [ + 0123456789,.,°,a,n, + "\u0020", # space + "\u003A", # colon + ] + - file: "gfonts://Roboto" + id: geyser_temperature_font2 + size: 60 + bpp: 4 + glyphs: [ + °,C, + ] + - file: "gfonts://Roboto" + id: geyser_temperature_font3 + size: 30 + bpp: 4 + glyphs: [ + b,o,m,p,t, + ] + - file: "fonts/misc/materialdesignicons-webfont.ttf" + id: font_icon_small + size: 24 #45 + glyphs: [ + "\U0000F5A9", + ] + +color: + - id: grey_light + hex: 'e0e0e0' + +#image: +## - file: https://esphome.io/_static/favicon-512x512.png +## id: boot_logo +## resize: 200x200 +## type: RGB565 +## transparency: alpha_channel +# - file: mdi:fire +# id: icon_fire +# resize: 100x100 +# type: BINARY +# - file: mdi:transmission-tower +# id: icon_utility +# resize: 80x80 +# type: BINARY +# +#psram: + +sun: + id: sun_sensor + latitude: !secret latitude + longitude: !secret longitude + +#interval: +# - interval: 10s +# then: +# - canbus.send: +# canbus_id: canbus_sthome +# data: [0x48, 0x45, 0x4C, 0x4C, 0x4F] +# - lambda: |- +# ESP_LOGI("SND:${CB_CANBUS_ID9}", "HELLO"); + +spi: + - id: spi_bus0 + clk_pin: GPIO18 + mosi_pin: GPIO23 + miso_pin: GPIO19 + interface: any + - id: spi_bus1 + clk_pin: GPIO17 + mosi_pin: GPIO16 + miso_pin: GPIO25 + interface: any + +#one_wire: +# - platform: gpio +# pin: GPIO4 +# id: temperature_sensors + +# CAN BUS +canbus: + - platform: mcp2515 + cs_pin: GPIO05 + spi_id: spi_bus1 + id: canbus_sthome + mode: LISTENONLY + can_id: ${CB_CANBUS_ID9} + #mode: NORMAL #LISTENONLY + bit_rate: 500KBPS + on_frame: + - can_id: 0 + can_id_mask: 0 + then: + - lambda: |- + id(dump_can_message).execute(x, can_id, remote_transmission_request); + - can_id: ${CB_BATTERY_STATE} + then: + - lambda: |- + auto value = static_cast((x[1] << 8) + x[0]); + id(battery_soc).publish_state(value); + value = static_cast((x[3] << 8) + x[2]); + id(battery_soh).publish_state(value); + - can_id: ${CB_BATTERY_STATUS} + then: + - lambda: |- + float value = 0.01 * static_cast((x[1] << 8) + x[0]); // unit = 0.01V Voltage of single module or average module voltage of system + // ESP_LOGW("REC: ${CB_BATTERY_STATUS}", "Voltage: %f", value); + id(battery_system_voltage).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A Module or system total current + // ESP_LOGW("REC: ${CB_BATTERY_STATUS}", "Current: %f", value); + id(battery_system_current).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1°C + id(battery_average_cell_temperature).publish_state(value); + - can_id: ${CB_BATTERY_SETTING} + then: + - lambda: |- + float value = 0.1 * ((x[1] << 8) + x[0]); // unit = 0.1V + id(battery_charge_voltage_limit).publish_state(value); + value = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A + id(battery_charge_current_limit).publish_state(value); + value = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1A + id(battery_discharge_current_limit).publish_state(value); + - can_id: ${CB_BATTERY_INFO} + then: + - lambda: |- + char buffer[16]; + 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); + - can_id: ${CB_BATTERY_REQUEST_FLAG} + then: + - lambda: |- + uint8_t request_flag = x[0]; + id(battery_charge_enable).publish_state(request_flag & 0x80); + id(battery_discharge_enable).publish_state(request_flag & 0x40); + id(battery_request_force_charge1).publish_state(request_flag & 0x20); + id(battery_request_force_charge2).publish_state(request_flag & 0x10); + id(battery_request_full_charge).publish_state( request_flag & 0x08); + - can_id: ${CB_BATTERY_MANUFACTURER} + then: + - lambda: |- + std::string str(x.begin(), x.end()); + id(battery_manufacturer).publish_state(str); + +# - can_id: ${CB_GEYSER_ENERGISED} +# then: +# - lvgl.widget.update: +# id: ind_geyser_on +# hidden: !lambda |- +# std::string on_state(x.begin(), x.end()); +# //ESP_LOGI("REC:${CB_GEYSER_ENERGISED}", "GEYSER IS: %s", on_state.c_str()); +# if(on_state == "ON") { +# id(g_geyser_heating_on) = true; +# return false; // not hidden +# } +# else if(on_state == "OFF") { +# id(g_geyser_heating_on) = false; +# return true; // hidden +# } +# //ESP_LOGW("REC:${CB_GEYSER_ENERGISED}", "Invalid ON/OFF value: %s", on_state.c_str()); +# return true; // default +# - can_id: ${CB_UTILITY_POWER_ON} +# then: +# - lvgl.widget.update: +# id: ind_utility_on +# hidden: !lambda |- +# std::string on_state(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_UTILITY_POWER_ON}", "UTILITY IS: %s", on_state.c_str()); +# if(on_state == "ON") { +# id(g_utility_on) = true; +# return false; // not hidden +# } +# else if(on_state == "OFF") { +# id(g_utility_on) = false; +# return true; // hidden +# } +# ESP_LOGW("REC:${CB_UTILITY_POWER_ON}", "Invalid ON/OFF value: %s", on_state.c_str()); +# return true; // default + +# - can_id: ${CB_GEYSER_TOP_TEMPERATURE} +# then: +# - lambda: |- +# id(update_temperature_display).execute(x, id(g_geyser_top_temperature), rect_gtoptemp, ind_utility_on, lbl_gtoptemp); +# +# - can_id: ${CB_GEYSER_BOTTOM_TEMPERATURE} +# then: +# - lambda: |- +# id(update_temperature_display).execute(x, id(g_geyser_bottom_temperature) , rect_gbottemp, ind_geyser_on, lbl_gbottemp); +# +# - can_id: ${CB_CANBUS_ID1} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID1}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID2} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID2}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID3} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID3}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID4} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID4}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID5} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID5}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID6} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID6}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID7} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID7}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID8} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID8}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID9} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID9}", "%s", &b[0] ); +# - can_id: ${CB_CANBUS_ID10} +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# ESP_LOGI("REC:${CB_CANBUS_ID10}", "%s", &b[0] ); + +# - can_id: 0x402 +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# //ESP_LOGI("canid 0x402", "%s", &b[0] ); +# ESP_LOGI("RECEIVED: canid 0x402", "%s", b.c_str()); +# - can_id: 0x400 +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# //ESP_LOGI("canid 0x400", "%s", &b[0] ); +# ESP_LOGI("RECEIVED: canid 0x400", "%s", b.c_str()); +# - can_id: 0x401 +# then: +# - lambda: |- +# std::string b(x.begin(), x.end()); +# //ESP_LOGI("canid 0x401", "%s", &b[0] ); +# ESP_LOGI("RECEIVED: canid 0x401", "%s", b.c_str()); + +#display: +# - platform: ili9xxx +# model: ili9488 +# id: tft_display +# color_palette: 8BIT +# data_rate: 40MHz +# spi_id: spi_bus0 +# cs_pin: GPIO15 +# dc_pin: GPIO2 +# reset_pin: GPIO27 +# auto_clear_enabled: false +# update_interval: never +# invert_colors: false +# show_test_card: true +# transform: +# swap_xy: true # landscape +## mirror_x: true # landscape +# dimensions: +# height: 480 +# width: 320 + +# Define a PWM output on the ESP32 +output: + - platform: ledc + pin: GPIO26 + id: backlight_pwm + +# Define a monochromatic, dimmable light for the backlight +light: + - platform: monochromatic + output: backlight_pwm + name: "Display Backlight" + id: back_light + restore_mode: ALWAYS_ON + +#touchscreen: +# platform: xpt2046 +# id: touch_screen +# spi_id: spi_bus0 +# cs_pin: GPIO33 +# transform: +# swap_xy: true # landscape +# # mirror_y: true # portrait +# calibration: +# x_min: 231 #201 #281 +# x_max: 3878 #3793 #3848 +# y_min: 221 #228 #347 +# y_max: 3861 #3914 #3878 +# +#lvgl: +## color_depth: 16 +## bg_color: 0x0F0F0F +# default_font: unscii_8 +## align: center +# theme: +# button: +# bg_color: grey_light #0x2F8CD8 +## bg_grad_color: 0x005782 +## bg_grad_dir: VER +# bg_opa: COVER +# border_color: 0x0077b3 +# border_width: 1 +# text_color: 0xFFFFFF +# pressed: # set some button colors to be different in pressed state +# bg_color: 0x006699 +# bg_grad_color: 0x00334d +# checked: # set some button colors to be different in checked state +# bg_color: 0x1d5f96 +# bg_grad_color: 0x03324A +# text_color: 0xfff300 +## switch: +## bg_color: 0xC0C0C0 +## bg_grad_color: 0xb0b0b0 +## bg_grad_dir: VER +## bg_opa: COVER +## checked: +## bg_color: 0x1d5f96 +## bg_grad_color: 0x03324A +## bg_grad_dir: VER +## bg_opa: COVER +## knob: +## bg_color: 0xFFFFFF +## bg_grad_color: 0xC0C0C0 +## bg_grad_dir: VER +## bg_opa: COVER +## slider: +## border_width: 1 +## border_opa: 15% +## bg_color: 0xcccaca +## bg_opa: 15% +## indicator: +## bg_color: 0x1d5f96 +## bg_grad_color: 0x03324A +## bg_grad_dir: VER +## bg_opa: COVER +## knob: +## bg_color: 0x2F8CD8 +## bg_grad_color: 0x005782 +## bg_grad_dir: VER +## bg_opa: COVER +## border_color: 0x0077b3 +## border_width: 1 +## text_color: 0xFFFFFF +# style_definitions: +# - id: header_footer +# bg_color: darkgrey #0x2F8CD8 +# bg_opa: COVER +# border_opa: TRANSP +# radius: 0 +# pad_all: 0 +# pad_row: 0 +# pad_column: 0 +# border_color: 0x0077b3 +# text_color: 0xFFFFFF +# width: 100% +# height: 30 +## - id: clockdate_style +## text_font: montserrat_20 #roboto_20 #unscii_8 +## text_align: center +## text_color: 0x000000 +## radius: 4 +## pad_all: 2 +## - id: sty_calendar_small +## radius: 0 +## pad_all: 0 +## pad_row: 0 +## pad_column: 0 +## text_font: unscii_8 +## shadow_opa: TRANSP +## text_color: black +## bg_color: white +## bg_opa: COVER +## border_color: grey_light +## border_width: 1 +## border_opa: cover #TRANSP +## - id: sty_calendar_small_noborders +## radius: 0 +## pad_all: 0 +## pad_row: 0 +## pad_column: 0 +## text_font: unscii_8 +## shadow_opa: TRANSP +## text_color: black +## bg_color: white +## bg_opa: COVER +## border_color: grey_light +## border_width: 0 +## border_opa: cover #TRANSP +# displays: +# - tft_display +# buffer_size: 12% +# top_layer: +# widgets: +# - label: +# text: "\U0000F5A9" # "\uF1EB" +# id: lbl_hastatus +# hidden: true +# align: top_right +# x: -2 +# y: 1 +# text_font: font_icon_small #montserrat_16 +# text_align: right +# text_color: 0x202020 # 0xFFFFFF +# - obj: # clipping rectangle +# x: 0 #15 +# y: -24 #7 +# pad_all: 0 +# height: 90 +# width: 65 +# align: BOTTOM_RIGHT +# bg_color: 0x000000 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: LV_OPA_TRANSP +# scrollbar_mode: "OFF" +# widgets: +# - image: +# id: ind_geyser_on +# align: CENTER #BOTTOM_RIGHT #TOP_RIGHT +# src: icon_fire +# image_recolor: RED +# image_recolor_opa: 100% +# x: 0 #15 #15 +# y: 0 #-22 #7 +# height: 100 #25 +# width: 100 #25 +# - obj: # clipping rectangle +# x: 0 #15 +# y: 2 #-24 #7 +# pad_all: 0 +# height: 80 +# width: 65 +# align: TOP_LEFT +# bg_color: 0x000000 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: LV_OPA_TRANSP +# scrollbar_mode: "OFF" +# widgets: +# - image: +# id: ind_utility_on +# align: CENTER #BOTTOM_RIGHT #TOP_RIGHT +# src: icon_utility +# image_recolor: grey #!lambda 'return lv_color_hex(0x000000);' +# image_recolor_opa: 100% +# x: 0 #15 #15 +# y: 0 #-22 #7 +# height: 80 #25 +# width: 80 #25 +## - obj: +## id: boot_screen +## x: 0 +## y: 0 +## width: 100% +## height: 100% +## bg_color: 0xffffff +## bg_opa: COVER +## radius: 0 +## pad_all: 0 +## border_width: 0 +## widgets: +## - image: +## align: CENTER +## src: boot_logo +## y: -40 +## - spinner: +## align: CENTER +## y: 95 +## height: 50 +## width: 50 +## spin_time: 1s +## arc_length: 60deg +## arc_width: 8 +## indicator: +## arc_color: 0x18bcf2 +## arc_width: 8 +## on_press: +## - lvgl.widget.hide: boot_screen +# - buttonmatrix: +# text_font: montserrat_16 +# align: bottom_mid +# styles: header_footer +# pad_all: 0 +# outline_width: 0 +# id: footer +# width: 480 +# items: +# styles: header_footer +# rows: +# - buttons: +# - id: page_prev +# text: "\uF053" +# on_press: +# then: +# lvgl.page.previous: +# - id: page_home +# text: "\uF015" +# on_press: +# then: +# lvgl.page.show: main_page +# - id: page_next +# text: "\uF054" +# on_press: +# then: +# lvgl.page.next: +# pages: +# # - id: pg_calendar +# # widgets: +# # - button: +# # id: cal_btn_prev_month +# # styles: sty_calendar_small +# # align: TOP_MID +# # pad_all: 0 +# # outline_width: 0 +# # border_color: black +# # border_width: 0 #1 +# # border_opa: TRANSP +# # x: -75 +# # y: 30 +# # width: 20 +# # height: 20 +# # bg_color: grey_light +# # text_color: 0xD3D3D3 +# # text_font: montserrat_14 +# # widgets: +# # - label: +# # align: center +# # text_font: montserrat_14 +# # text: "<" +# # on_press: +# # then: +# # lambda: |- +# # id(update_calendar_month).execute(-1); +# # - dropdown: +# # id: cal_dd_year +# # styles: sty_calendar_small +# # text_font: montserrat_12 +# # height: 20 +# # width: 55 +# # radius: 0 +# # align_to: +# # id: cal_btn_prev_month +# # align: out_right_top +# # x: 80 +# # y: 0 #12.5% +# # options: +# # - 2024 +# # - 2025 +# # selected_index: 0 +# # dropdown_list: +# # text_line_space: 3 +# # pad_all: 1 +# # text_font: unscii_8 +# # max_height: 260 +# # radius: 0 +# # selected: +# # checked: +# # text_color: 0xFF0000 +# # on_value: +# # then: +# # - lambda: |- +# # id(update_calendar).execute(); +# # - dropdown: +# # id: cal_dd_month +# # styles: sty_calendar_small +# # text_font: montserrat_12 +# # height: 20 +# # width: 55 +# # radius: 0 +# # align_to: +# # id: cal_dd_year +# # align: out_right_top +# # x: 0 +# # y: 0 #12.5% +# # options: +# # - Jan +# # - Feb +# # - Mar +# # - Apr +# # - May +# # - Jun +# # - Jul +# # - Aug +# # - Sep +# # - Oct +# # - Nov +# # - Dec +# # selected_index: 0 +# # dropdown_list: +# # text_line_space: 3 +# # pad_all: 1 +# # text_font: unscii_8 +# # max_height: 260 +# # radius: 0 +# # selected: +# # checked: +# # text_color: 0xFF0000 +# # on_value: +# # then: +# # - lambda: |- +# # id(update_calendar).execute(); +# # - button: +# # id: cal_btn_next_month +# # styles: sty_calendar_small +# # align_to: +# # id: cal_dd_month +# # align: out_right_top +# # x: 0 +# # y: 0 +# # pad_all: 0 +# # outline_width: 0 +# # border_color: black +# # border_width: 0 #1 +# # border_opa: TRANSP +# # x: -75 +# # y: 30 +# # width: 20 +# # height: 20 +# # bg_color: grey_light +# # text_color: 0xD3D3D3 +# # text_font: montserrat_14 +# # widgets: +# # - label: +# # align: center +# # text_font: montserrat_14 +# # text: ">" +# # on_press: +# # then: +# # lambda: |- +# # id(update_calendar_month).execute(1); +# # - buttonmatrix: +# # id: bmx_cal_header_dow +# # styles: sty_calendar_small_noborders +# # align_to: +# # id: cal_btn_prev_month +# # align: out_bottom_left +# # x: 80 +# # y: 0 #12.5% +# # pad_all: 0 +# # outline_width: 0 +# # border_color: black +# # border_width: 0 #1 +# # border_opa: TRANSP +# # x: 0 +# # y: 0 +# # width: 150 +# # height: 20 +# # bg_color: black +# # text_color: 0xD3D3D3 +# # items: +# # styles: sty_calendar_small_noborders +# # pressed: +# # bg_color: 0x006699 +# # bg_grad_color: 0x00334d +# # checked: +# # bg_color: 0x1d5f96 +# # bg_grad_color: 0x03324A +# # rows: +# # - buttons: +# # - id: r0c1 +# # text: "Su" +# # width: 1 +# # - id: r0c2 +# # text: "Mo" +# # width: 1 +# # - id: r0c3 +# # text: "Tu" +# # width: 1 +# # - id: r0c4 +# # text: "We" +# # width: 1 +# # - id: r0c5 +# # text: "Th" +# # width: 1 +# # - id: r0c6 +# # text: "Fr" +# # width: 1 +# # - id: r0c7 +# # text: "Sa" +# # width: 1 +# # on_press: +# # then: +# # - lambda: |- +# # ESP_LOGI("day of week", "%d", x); +# # - buttonmatrix: +# # id: bmx_calendar +# # styles: sty_calendar_small +# # align_to: +# # id: bmx_cal_header_dow +# # align: out_bottom_left +# # x: 0 +# # y: 0 #12.5% +# # pad_all: 0 +# # outline_width: 0 +# # border_color: black +# # border_width: 0 #1 +# # border_opa: TRANSP +# # x: 0 +# # y: 0 +# # width: 150 +# # height: 100 +# # bg_color: black +# # text_color: 0xD3D3D3 +# # items: +# # styles: sty_calendar_small +# # pressed: +# # bg_color: 0x006699 +# # bg_grad_color: 0x00334d +# # checked: +# # bg_color: 0x1d5f96 +# # bg_grad_color: 0x03324A +# # rows: +# # - buttons: +# # - id: r1c1 +# # text: " " +# # width: 1 +# # control: +# # recolor: true +# # - id: r1c2 +# # text: " " +# # width: 1 +# # - id: r1c3 +# # text: "1" +# # width: 1 +# # - id: r1c4 +# # text: "2" +# # width: 1 +# # - id: r1c5 +# # text: "3" +# # width: 1 +# # - id: r1c6 +# # text: "4" +# # width: 1 +# # - id: r1c7 +# # text: "5" +# # width: 1 +# +# # # Define actions on button press +# # on_press: +# # then: +# # lambda: |- +# # //lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); +# # //lv_btnmatrix_set_one_checked(bmx_calendar->obj, false); +# # //id(get_calendar_days_state).execute("P1"); +# # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); +# # //ESP_LOGI("on press", "day: %s, stat: %d", lv_btnmatrix_get_btn_text(bmx_calendar->obj, x), stat); +# # //id(get_calendar_days_state).execute("P2"); +# # on_release: +# # then: +# # lambda: |- +# # id(get_calendar_days_state).execute("R1"); +# # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); +# # //auto* day = lv_btnmatrix_get_btn_text(bmx_calendar->obj, x); +# # // if(stat) { +# # // lv_btnmatrix_clear_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); +# # // } +# # // else { +# # // lv_btnmatrix_set_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); +# # // } +# # //auto stat = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, x, LV_BTNMATRIX_CTRL_CHECKED); +# # //ESP_LOGI("on relse", "day: %s, stat: %d", lv_btnmatrix_get_btn_text(bmx_calendar->obj, x), stat); +# # //id(get_calendar_days_state).execute("R2"); +# +# - id: main_page #pg_geyser_temp +# widgets: +# - obj: +# id: rect_gtoptemp +# x: 0 +# y: 0 #30 +# pad_all: 0 +# height: 290 +# width: 240 +# align: TOP_LEFT +# bg_color: 0x000000 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: COVER +# - obj: +# id: rect_gbottemp +# y: 0 +# pad_all: 0 +# height: 290 +# width: 240 +# align_to: +# id: rect_gtoptemp +# align: out_right_top +# x: 0 +# y: 0 #12.5% +# bg_color: 0x000000 #0xFF4500 +# border_color: 0xFFFFFF +# border_width: 0 +# radius: 0 +# bg_opa: COVER +# - label: +# text: " " +# id: lbl_gtoptemp +# hidden: false +# align: LEFT_MID +# x: 0 +# y: -10 +# text_font: roboto_200 +# text_align: center +# text_color: 0x0 +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff +# - label: +# text: " " +# id: lbl_gbottemp +# hidden: false +# align: RIGHT_MID +# x: 0 +# y: -10 +# text_font: roboto_200 +# text_align: center +# text_color: 0x0 +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff +## - label: +## text: "°C" +## id: lbl_degree +## hidden: false +## align: BOTTOM_MID +## x: 0 +## y: -30 +## text_font: geyser_temperature_font2 +## text_align: center +## text_color: 0x0 +## bg_opa: LV_OPA_TRANSP +## bg_color: 0xffffff +# - label: +# text: "top" +# id: lbl_top +# hidden: false +# align: TOP_MID +# x: -120 +# y: 20 +# text_font: geyser_temperature_font3 +# text_align: center +# text_color: 0x0 +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff +# - label: +# text: "bottom" +# id: lbl_bottom +# hidden: false +# align: TOP_MID +# x: 120 +# y: 20 +# text_font: geyser_temperature_font3 +# text_align: center +# text_color: 0x0 +# bg_opa: LV_OPA_TRANSP +# bg_color: 0xffffff +# +## - id: pg_settings +## widgets: +## - textarea: +## id: geyser_schedule +## one_line: true +## placeholder_text: "Enter text here" +## - keyboard: +## id: keyboard_id +## textarea: geyser_schedule +## mode: TEXT_UPPER +## text_font: montserrat_20 +## on_focus: +## then: +## - lvgl.keyboard.update: +## id: keyboard_id +## mode: number +## textarea: geyser_schedule +## on_ready: +## then: +## - logger.log: Keyboard is ready +## on_cancel: +## then: +## - logger.log: Keyboard cancelled# +# +## - id: pg_clock +## widgets: +## - obj: # clock container +## height: 300 #SIZE_CONTENT +## width: 300 # 100% +## align: TOP_MID +## pad_all: 0 +## border_width: 0 +## bg_color: 0xFFFFFF +## widgets: +## - meter: # clock face +## height: 300 +## width: 300 +## align: TOP_MID +## bg_opa: TRANSP +## border_width: 0 +## text_color: 0x000000 +## scales: +## - range_from: 0 # minutes scale +## range_to: 720 +## angle_range: 360 +## rotation: 270 +## ticks: +## width: 1 +## count: 61 +## length: 10 +## color: 0x000000 +## indicators: +## - line: +## id: minute_hand +## width: 3 +## color: 0xa6a6a6 +## r_mod: -4 +## value: 0 +## - range_from: 1 # hours scale for labels +## range_to: 12 +## angle_range: 330 +## rotation: 300 +## ticks: +## width: 1 +## count: 12 +## length: 1 +## major: +## stride: 1 +## width: 4 +## length: 10 +## color: 0xC0C0C0 +## label_gap: 12 +## - range_from: 0 # hi-res hours scale for hand +## range_to: 720 +## angle_range: 360 +## rotation: 270 +## ticks: +## count: 0 +## indicators: +## - line: +## id: hour_hand +## width: 5 +## color: 0xa6a6a6 +## r_mod: -30 +## value: 0 +## # Second hand +## - angle_range: 360 +## rotation: 270 +## range_from: 0 +## range_to: 60 +## indicators: +## - line: +## id: second_hand +## width: 2 +## color: Red +## r_mod: -10 +## - label: +## align: CENTER +## styles: clockdate_style +## id: day_label +## y: -50 +## - label: +## align: CENTER +## id: date_label +## styles: clockdate_style +## y: 50 +# +## - id: pg_digital_clock +## widgets: +## - obj: +## id: rect_gtoptemp1 +## x: 0 +## y: 0 #30 +## pad_all: 0 +## height: 290 +## width: 240 +## align: TOP_LEFT +## bg_color: 0x000000 +## border_color: 0xFFFFFF +## border_width: 0 +## radius: 0 +## bg_opa: COVER +## - obj: +## id: rect_gbottemp1 +## y: 0 +## pad_all: 0 +## height: 290 +## width: 240 +## align_to: +## id: rect_gtoptemp +## align: out_right_top +## x: 0 +## y: 0 #12.5% +## bg_color: 0x000000 #0xFF4500 +## border_color: 0xFFFFFF +## border_width: 0 +## radius: 0 +## bg_opa: COVER +## - label: +## text: " " +## id: lbl_digitalclock +## hidden: false +## align: TOP_MID +## x: 0 +## y: 20 +## text_font: roboto_192 +## text_align: center +## text_color: RED +## bg_opa: LV_OPA_TRANSP +## bg_color: 0xffffff +# +#switch: +# - platform: restart +# name: "${name} Restart" +# id: "restart_switch" +## - platform: template +## name: Antiburn +## id: switch_antiburn +## icon: mdi:television-shimmer +## optimistic: true +## entity_category: "config" +## turn_on_action: +## - logger.log: "Starting Antiburn" +## - if: +## condition: lvgl.is_paused +## then: +## - lvgl.resume: +## - lvgl.widget.redraw: +## - lvgl.pause: +## show_snow: true +## turn_off_action: +## - logger.log: "Stopping Antiburn" +## - if: +## condition: lvgl.is_paused +## then: +## - lvgl.resume: +## - lvgl.widget.redraw: + +binary_sensor: + - 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;" + +sensor: + - 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 SOH" + 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 + - 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 + - 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 + + +# - platform: dallas_temp +# address: 0xfe00000037b3d528 +# name: "Study Temperature" +# id: study_temperature +# update_interval: "60s" +# resolution: 12 +# one_wire_id: 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 + id: wifi_sig + 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: 60s + +text_sensor: +## - 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: 60s +# +# # Expose WiFi information as sensors +# - platform: wifi_info +# ip_address: +# name: IP +# mac_address: +# name: Mac Address +# entity_category: diagnostic +# ssid: +# name: "Connected SSID" +# id: ssid +# entity_category: diagnostic +# +# # human readable update text sensor from sensor:uptime +# - platform: template +# name: Uptime +# id: uptime_human +# icon: mdi:clock-start +# +# - platform: template +# name: 'Last Restart' +# id: device_last_restart +# icon: mdi:clock +# entity_category: diagnostic + - platform: template + id: battery_manufacturer + name: "Battery Manufacturer" + - platform: template + id: battery_module_numbers + name: "Battery Module Numbers" + +script: +# - id: update_temperature_display +# parameters: +# x: std::vector& +# globalvar: double& +# rect: lv_obj_t* +# indicator: lv_obj_t* +# label: lv_obj_t* +# then: +# - lambda: |- +# char buffer [4]; +# buffer[0] = '\0'; +# double value = x[3] + ((double)((x[2] << 16) + (x[1] << 8) + x[0]))/16777216; +# globalvar = value; +# snprintf (buffer, 4, "%.0f", value); +# auto bgcolor = lv_color_hex(0xFF0000); +# auto ind_color = lv_color_hex(0xFF0000); +# if(value < 40) { +# bgcolor = lv_color_hex(0x0000FF); +# } +# else if(value < 50) { +# bgcolor = lv_color_hex(0x00FF00); +# } +# else if(value < 60) { +# bgcolor = lv_color_hex(0xFFFF00); +# } +# else { +# ind_color = lv_color_hex(0xFFFF00); // make different to bgcolor +# } +# lv_obj_set_style_bg_color(rect, bgcolor, LV_PART_MAIN); +# lv_obj_set_style_img_recolor(indicator, ind_color, LV_PART_MAIN); +# lv_label_set_text(label, buffer); + +# - id: time_update +# then: +# - lvgl.indicator.update: +# id: minute_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return now.minute * 12 + now.second/5; +# - lvgl.indicator.update: +# id: hour_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return std::fmod(now.hour, 12) * 60 + now.minute; +# - lvgl.indicator.update: +# id: second_hand +# value: !lambda |- +# auto now = id(time_source).now(); +# return now.second; +# - lvgl.label.update: +# id: date_label +# text: !lambda |- +# static const char * const mon_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +# static char date_buf[8]; +# auto now = id(time_source).now(); +# snprintf(date_buf, sizeof(date_buf), "%s %2d", mon_names[now.month-1], now.day_of_month); +# return date_buf; +# - lvgl.label.update: +# id: day_label +# text: !lambda |- +# static const char * const day_names[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; +# return day_names[id(time_source).now().day_of_week - 1]; +# - lvgl.label.update: +# id: lbl_digitalclock +# text: !lambda |- +# auto time_obj = id(time_source).now(); +# return time_obj.strftime("%H:%M"); + +# - id: ind_heating_update +# then: +# - lvgl.widget.update: +# id: ind_geyser_on +# hidden: !lambda return !id(g_geyser_heating_on); + +# - id: init_calendar +# then: +# - lambda: |- +# auto now = id(time_source).now(); +# //ESP_LOGI("yopts before", stroptions.c_str()); +# int y = 0; +# std::string stroptions = to_string(now.year + y); +# while(++y < ${DD_MAX_YEARS}) { +# stroptions += "\n" + to_string(now.year + y); +# } +# //ESP_LOGI("yopts after", stroptions.c_str()); +# lv_dropdown_set_options(cal_dd_year->obj, stroptions.c_str()); +# lv_dropdown_set_selected(cal_dd_year->obj, 0); // this year is first index +# lv_dropdown_set_selected(cal_dd_month->obj, now.month-1); +# id(g_year_idx) = 0; +# id(update_calendar).execute(); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); + +# - id: update_calendar_month +# parameters: +# increment : int +# then: +# - lambda: |- +# char yearstr[8]; +# lv_dropdown_get_selected_str(cal_dd_year->obj, yearstr, sizeof(yearstr)); +# auto year = atoi(yearstr); +# int year_idx = lv_dropdown_get_selected(cal_dd_year->obj); +# int month_idx = increment + lv_dropdown_get_selected(cal_dd_month->obj); +# int month = 1 + month_idx; +# if(month > 12 && year_idx < ${DD_MAX_YEARS} - 1) { +# month -= 12; +# month_idx -= 12; +# year++; +# year_idx++; +# } +# else if(month < 1 && year_idx > 0) { +# month += 12; +# month_idx += 12; +# year--; +# year_idx--; +# } +# ESP_LOGI("cm", "month: %d, year: %d", month, year); +# if(month < 13 && month > 0) { +# lv_dropdown_set_selected(cal_dd_year->obj, year_idx); +# lv_dropdown_set_selected(cal_dd_month->obj, month_idx); +# id(g_year_idx) = year_idx; +# id(update_calendar).execute(); +# } +# +# - id: update_calendar +# then: +# lambda: |- +# char yearstr[8]; +# int monthdays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +# static std::string strdays[44]; +# static const char *pstrdays[49]; // including newline at end of week +# static const char *newline = "\n"; +# id(persist_calendar).execute(); +# lv_dropdown_get_selected_str(cal_dd_year->obj, yearstr, sizeof(yearstr)); +# int year = atoi(yearstr); +# int month = 1 + lv_dropdown_get_selected(cal_dd_month->obj); +# bool isLeapYear = (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0)); +# monthdays[1] = (isLeapYear) ? 29 : 28; +# // calculate day of week of 1st of month using Zeller's rule +# // https://beginnersbook.com/2013/04/calculating-day-given-date +# // modified month, year +# int mM = month - 2; +# int m = mM < 1 ? 12 + mM : mM; +# int mY = mM < 1 ? year - 1 : year; +# int k = 1; // day of month +# int D = mY % 100; // last two digits of the year +# int C = trunc(mY / 100); // first two digits of the year +# int F = k + trunc((13 * m - 1) / 5) + D + trunc(D / 4) + trunc(C / 4) - 2 * C; +# int Z = F % 7; +# int start_of_month = Z < 0 ? Z + 7 : Z; +# // end of Zeller's rule +# int previous_month = (month == 1) ? 12 : month - 1; +# int month_days = monthdays[month - 1]; +# int prev_month_days = monthdays[previous_month - 1]; +# int i = 0; +# int j = -1; +# //ESP_LOGI("vals", "start_of_month: %d, previous_month: %d, month_days: %d, prev_month_days: %d", start_of_month, previous_month, month_days, prev_month_days); +# for (int w = 0; w < 6 && i < (month_days + start_of_month); w++) { +# for(int wd = 0; wd < 7; wd++) { +# int day = i + 1 - start_of_month; +# if (i < start_of_month) { +# day += prev_month_days; +# strdays[i] = "#e0e0e0 " + to_string(day) + "#"; +# } +# else if (i >= (month_days + start_of_month)) { +# day -= month_days; +# strdays[i] = "#e0e0e0 " + to_string(day) + "#"; +# } +# else { +# strdays[i] = to_string(day); +# } +# pstrdays[++j] = strdays[i].c_str(); +# //ESP_LOGI("bmx", "%s, i: %d, j: %d", pstrdays[j], i, j); +# i++; +# } +# pstrdays[++j] = newline; +# //ESP_LOGI("bmxnl", "%s, i: %d, j: %d", pstrdays[j], i, j); +# } +# pstrdays[j] = NULL; // terminator, overwrites last newline +# //ESP_LOGW("day", "terminating at: %d", j); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); +# lv_btnmatrix_set_map(bmx_calendar->obj, pstrdays); +# lv_btnmatrix_set_btn_ctrl_all(bmx_calendar->obj, LV_BTNMATRIX_CTRL_CHECKABLE | LV_BTNMATRIX_CTRL_RECOLOR); +# +# - id: persist_calendar +# then: +# lambda: |- +# id(g_year_idx) = lv_dropdown_get_selected(cal_dd_year->obj); +# id(g_month_idx) = lv_dropdown_get_selected(cal_dd_month->obj); +# // copy year options to persistent globals +# const char* opts = lv_dropdown_get_options(cal_dd_year->obj); +# int opt_store_size = sizeof(id(g_options_year)); +# strncpy(id(g_options_year), opts, opt_store_size); +# id(g_options_year)[opt_store_size] = '\0'; +# //ESP_LOGI("year options", id(g_options_year)); +# +# - id: get_calendar_days_state +# parameters: +# flag: std::string +# then: +# lambda: |- +# // count buttons +# int num_buttons = 0; +# auto* buttonmap = lv_btnmatrix_get_map(bmx_calendar->obj); +# int i = 0; +# for (; buttonmap[i] != NULL && buttonmap[i][0] != '\0' && i < 48; i++) { +# bool isNewLine = strcmp(buttonmap[i], "\n") == 0; +# if (!isNewLine) { +# num_buttons++; +# } +# } +# std::string sch_holidays = ""; +# std::string pub_holidays = ""; +# std::string vac_days = ""; +# int h = 0; +# for(int i = 0; i < num_buttons; i++) { +# bool isSchoolHoliday = lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CHECKED); +# bool isPublicHoliday = false; //lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CUSTOM_1); +# bool isVacationDay = false; //lv_btnmatrix_has_btn_ctrl(bmx_calendar->obj, i, LV_BTNMATRIX_CTRL_CUSTOM_2); +# if(isSchoolHoliday || isPublicHoliday || isVacationDay) { +# sch_holidays = sch_holidays + lv_btnmatrix_get_btn_text(bmx_calendar->obj, i) + " "; +# h++; +# } +# } +# if(h > 0) { +# ESP_LOGI("day", "[%s] s: %s \tp: %s \tv: %s", flag.c_str(), sch_holidays.c_str(), pub_holidays.c_str(), vac_days.c_str()); +# } +- id: dump_can_message + parameters: + x: std::vector& + can_id: uint32_t + remote_transmission_request: bool + then: + lambda: |- + char buffer[80]; + char tag[16]; + // comment out following few lines if you don't want to deduplicate consecutive messages + auto& y = id(can_lastframe); + bool isduplicate = can_id == id(can_lastid); + auto j = y.begin(); + if(x.size() == y.size()) { + for(auto i = x.begin(); i != x.end() && isduplicate; ++i) { + isduplicate = isduplicate && (*i == *j++); + } + } + if(isduplicate) { + return; + } + else { + y.clear(); + y.insert(y.end(), x.begin(), x.end()); + id(can_lastid) = can_id; + } + // end of deduplication + snprintf(tag, sizeof(tag), "CAN REC: 0x%X", can_id); + snprintf(buffer, sizeof(buffer), " %d ", remote_transmission_request); + std::string text = ""; + std::string line = buffer; + std::string decoded = ""; + for(auto i = x.begin(); i != x.end(); ++i) { + auto byte = *i; + snprintf(buffer, sizeof(buffer), " %02X", byte); + line += buffer; + if(byte > 31 && byte < 127) { + text += (char) byte; + } + else { + text += "."; + } + } + switch (can_id) + { + case ${CB_BATTERY_SETTING}: + { + float battery_charge_voltage_limit = 0.1 * ((x[1] << 8) + x[0]); // unit = 0.1V + float charge_current_limit = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A + float discharge_current_limit = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1A + snprintf(buffer, sizeof(buffer), "BATTERY SETTINGS: VMax= %.1fV IMaxChg= %.1fA IMaxDis= %.1fA", battery_charge_voltage_limit, charge_current_limit, discharge_current_limit); + decoded += buffer; + } + break; + case ${CB_BATTERY_STATE}: + { + uint soc = static_cast((x[1] << 8) + x[0]); + uint soh = static_cast((x[3] << 8) + x[2]); + snprintf(buffer, sizeof(buffer), "BATTERY STATE: SOC= %d%% SOH= %d%%", soc, soh); + decoded += buffer; + } + break; + case ${CB_BATTERY_STATUS}: + { + float system_voltage = 0.01 * static_cast((x[1] << 8) + x[0]); // unit = 0.01V Voltage of single module or average module voltage of system + float system_current = 0.1 * static_cast((x[3] << 8) + x[2]); // unit = 0.1A Module or system total current + float average_cell_temperature = 0.1 * static_cast((x[5] << 8) + x[4]); // unit = 0.1°C + snprintf(buffer, sizeof(buffer), "BATTERY STATUS: VSYS= %.2fV ISYS= %.1fA TSYS= %.1f°C", system_voltage, system_current, average_cell_temperature); + decoded += buffer; + } + break; + case ${CB_BATTERY_INFO}: + { + 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]; + bool discharge_over_current = protection1 & 0x80; + bool cell_under_temperature = protection1 & 0x10; + bool cell_over_temperature = protection1 & 0x08; + bool cell_or_module_under_voltage = protection1 & 0x04; + bool cell_or_module_over_voltage = protection1 & 0x02; + bool system_error = protection2 & 0x8; + bool charge_over_current = protection2 & 0x01; + bool discharge_high_current = alarm1 & 0x80; + bool cell_low_temperature = alarm1 & 0x10; + bool cell_high_temperature = alarm1 & 0x08; + bool cell_or_module_low_voltage = alarm1 & 0x04; + bool cell_or_module_high_voltage = alarm1 & 0x02; + bool internal_communication_fail = alarm2 & 0x8; + bool charge_high_current = alarm2 & 0x01; + snprintf(buffer, sizeof(buffer), "BATTERY PROTECT: %s%s%s%s%s%s%s ALARM= %s%s%s%s%s%s%s MN=%d %c%c", discharge_over_current ? "DOC " : "", cell_under_temperature ? "CUT " : "", cell_over_temperature ? "COT " : "", cell_or_module_under_voltage ? "CMUV " : "", cell_or_module_over_voltage ? "CMOV" : "", system_error ? "SERR " : "", charge_over_current ? "COC ": "", discharge_high_current ? "DHC " : "", cell_low_temperature ? "CLT " : "", cell_high_temperature ? "CHT " : "", cell_or_module_low_voltage ? "CMLV " : "", cell_or_module_high_voltage ? "CMHV" : "", internal_communication_fail ? "ICF " : "", charge_high_current ? "CHC ": "", module_numbers, ch5, ch6); + decoded += buffer; + } + break; + case ${CB_BATTERY_REQUEST_FLAG}: + { + uint8_t request_flag = x[0]; + bool charge_enable = request_flag & 0x80; + bool discharge_enable = request_flag & 0x40; + bool request_force_charge1 = request_flag & 0x20; // use bit 5, the SOC range is: 15~19%. Bit 4 is NULL. Bit 5 is designed for inverter allows battery to shut down, and able to wake battery up to charge it. + bool request_force_charge2 = request_flag & 0x10; // Bit 5 the SOC range is 5~10%, Bit 4 the SOC range is 9~13%. Bit 4 is designed for inverter doesn`t want battery to shut down, able to charge battery before shut down to avoid low energy. We suggest inverter to use this bit, In this case, inverter itself should set a threshold of SOC: after force charge, only when battery SOC is higher than this threshold then inverter will allow discharge, to avoid force charge and discharge status change frequently. + bool request_full_charge = request_flag & 0x08; // Reason: when battery is not full charged for long time, the accumulative error of SOC calculation will be too high and may not able to be charged or discharged as expected capacity. Logic: if SOC never higher than 97% in 30 days, will set this flag to 1. And when the SOC is 97%, the flag will be 0. How to: we suggest inverter to charge the battery by grid when this flag is 1. + snprintf(buffer, sizeof(buffer), "BATTERY REQUEST: %s%s%s%s%s", charge_enable ? "CE " : "", discharge_enable ? "DE " : "", request_force_charge1 ? "RFORCECH1 " : "", request_force_charge2 ? "RFORCECH2 " : "", request_full_charge ? "RFULLCH" : ""); + decoded += buffer; + } + break; + case ${CB_BATTERY_MANUFACTURER}: + { + std::string manufacturer(x.begin(), x.end()); + decoded += "BATTERY OEM: " + manufacturer; + } + break; + } + ESP_LOGI(tag, "%s %s %s", line.c_str(), text.c_str(), decoded.c_str()); \ No newline at end of file