From dc9b02515dd0db25a742a62a988d98bfa3c90829 Mon Sep 17 00:00:00 2001 From: sftman18 Date: Tue, 27 Feb 2024 15:33:04 -0600 Subject: [PATCH 1/8] Changed starting/stopping/report delays from loop cycles to elapsed seconds --- PVCharge.py | 85 ++++++++++++++++++++++++--------------------- example_config.toml | 9 +++-- routines.py | 18 ++++++++-- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/PVCharge.py b/PVCharge.py index 44773bf..cde8aa1 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -1,16 +1,16 @@ import os import math +import time import logging import tomllib -from time import sleep -from routines import PowerUsage, TeslaCommands, MqttCallbacks +import routines # Load config file with open("config.toml", mode="rb") as fp: config = tomllib.load(fp) logging.basicConfig( - filename=config["LOG_FILE"], + #filename=config["LOG_FILE"], level=logging.INFO, format='%(asctime)s %(levelname)s %(module)s - %(funcName)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', @@ -24,19 +24,18 @@ # Initialize classes -Energy = PowerUsage() -Car = TeslaCommands() -Messages = MqttCallbacks() +Energy = routines.PowerUsage() +Car = routines.TeslaCommands() +Messages = routines.MqttCallbacks() # Control loop variables car_is_charging = False -report_due_fast = 0 -report_due_slow = 0 -report_delay_fast = round((60 / (config["FAST_POLLING"] + 1.5))) # Print a report roughly every minute -report_delay_slow = round((60 / config["SLOW_POLLING"])) # Print a report roughly every minute -start_charging_count = 0 -stop_charging_count = 0 +stop_charging_time = 0 +start_charging_time = 0 +report_time = 0 while True: + # Record loop start time + loop_time = time.time() # Check if we are allowed to charge charge_tesla = Messages.calculate_charge_tesla() logging.debug(f"Current calculated charge enable: {charge_tesla}") @@ -56,19 +55,20 @@ logging.warning("Slow poll, Car discovered charging and was NOT stopped successfully") else: logging.info("Slow poll wait, ignore charging") - sleep(config["SLOW_POLLING_CHK"]) - if report_due_slow >= report_delay_slow: - status = Energy.status_report(charge_tesla, car_is_charging, new_sample=True) - logging.info(f"Slow poll {status}") - Messages.client.publish(topic=config["TOPIC_STATUS"], payload=status, qos=1) - report_due_slow = 0 - report_due_slow += 1 + # Wait configured time before reporting status + report_is_due, report_time = routines.check_elapsed_time(loop_time, report_time, config["REPORT_DELAY"]) + if report_is_due: + status = Energy.status_report(charge_tesla, car_is_charging, new_sample=True) + logging.info(f"{status}") + Messages.client.publish(topic=config["TOPIC_STATUS"], payload=status, qos=1) + report_time = 0 + time.sleep(config["SLOW_POLLING_CHK"]) if charge_tesla: # If we are allowed to charge if car_is_charging: # Is the car currently charging? if Energy.sufficient_generation(config["MIN_CHARGE"]): - # Reset stop counter - stop_charging_count = 0 + # Reset stop time + stop_charging_time = 0 # Calculate new charge rate # Use math.floor() on calculate_charge_rate to ensure we are always just "under" the available PV generation capacity # Use round() on charge_rate_sensor to prevent constant requests when on the edge of a value @@ -91,41 +91,47 @@ Messages.client.publish(topic=config["TOPIC_CHARGE_RATE"], payload=config["MIN_CHARGE"], qos=1) else: logging.warning(f"Car charging, Available Energy Reduced, new rate was NOT successfully set") - else: # We are already at min charge, begin stopping sequence - stop_charging_count += 1 - logging.info(f"Car charging, Available Energy Reduced, charging at min rate, stopping count: {stop_charging_count}") - if stop_charging_count >= 30: + + else: # We are already at min charge + # Wait configured time before stopping + waited_long_enough, stop_charging_time = routines.check_elapsed_time(loop_time, stop_charging_time, config["DELAYED_STOP_TIME"]) + if waited_long_enough: if Car.stop_charging(): logging.info(f"Car charging, Available Energy Reduced, charging was successfully stopped") car_is_charging = False - stop_charging_count = 0 + stop_charging_time = 0 else: logging.warning(f"Car charging, Available Energy Reduced, charging was NOT successfully stopped") + else: + logging.info(f"Car charging, Available Energy Reduced, charging at min rate, stopping in: {config['DELAYED_STOP_TIME'] - loop_time - stop_charging_time} seconds") else: # Car isn't charging, should it be? if Energy.sufficient_generation(config["MIN_CHARGE"]): # If we have enough sun to charge if round(Energy.charge_rate_sensor) < config["MIN_CHARGE"]: # Make sure car isn’t already charging - start_charging_count += 1 - logging.info(f"Car is NOT charging, Energy is Available, starting count: {start_charging_count}") - if start_charging_count >= 5: + # Wait configured time before starting + waited_long_enough, start_charging_time = routines.check_elapsed_time(loop_time, start_charging_time, config["DELAYED_START_TIME"]) + if waited_long_enough: if Car.wake(): logging.info(f"Car is NOT charging, Energy is Available, car woken successfully") - sleep(5) # Wait until car is awake + time.sleep(5) # Wait until car is awake if Car.start_charging(): logging.info(f"Car Started Charging Successfully") - sleep(10) # Wait until charging is fully started + time.sleep(10) # Wait until charging is fully started if Energy.verify_new_charge_rate(config["MIN_CHARGE"]): logging.info(f"Charge Rate is greater than min charge") car_is_charging = True - start_charging_count = 0 + start_charging_time = 0 # Optionally we could set a new charge rate here else: logging.warning(f"Car Charging NOT Started Successfully") else: logging.warning(f"Car was NOT woken successfully") + else: + logging.info(f"Car is NOT charging, Energy is Available, starting in: {config['DELAYED_START_TIME'] - loop_time - start_charging_time} seconds") + else: # Car is already charging, set the flag car_is_charging = True - start_charging_count = 0 + start_charging_time = 0 else: # Sun isn't generating enough power to charge if prevent_non_solar_charge: # If true, prevent after-hours charging @@ -146,12 +152,13 @@ logging.info(f"Charge Stopping, did NOT stop successfully") car_is_charging = False # Clear the flag even if it fails - if report_due_fast >= report_delay_fast: + # Wait configured time before reporting status + report_is_due, report_time = routines.check_elapsed_time(loop_time, report_time, config["REPORT_DELAY"]) + if report_is_due: status = Energy.status_report(charge_tesla, car_is_charging, new_sample=True) - logging.info(f"Fast poll {status}") + logging.info(f"{status}") Messages.client.publish(topic=config["TOPIC_STATUS"], payload=status, qos=1) - report_due_fast = 0 - report_due_fast += 1 + report_time = 0 - # Main loop delay - sleep(config["FAST_POLLING"]) + # Control loop delay + time.sleep(config["FAST_POLLING"]) diff --git a/example_config.toml b/example_config.toml index 705701b..b3c9a31 100644 --- a/example_config.toml +++ b/example_config.toml @@ -12,8 +12,11 @@ TOPIC_STATUS = "topic_base/status" TOPIC_CHARGE_RATE = "topic_base/new_charge_rate" # Control loop parameters -SLOW_POLLING = 30 # Charging disabled, control topic check interval (seconds) -SLOW_POLLING_CHK = 5 # Charging disabled, prevent non-solar charge check interval (seconds) -FAST_POLLING = 1 # Charging enabled, update rate (seconds) MIN_CHARGE = 7 # Slowest allowed charge rate (Amps) MAX_CHARGE_LIMIT = 80 # Max charge limit (%) +SLOW_POLLING = 30 # Charging disabled, control topic check interval (seconds) +SLOW_POLLING_CHK = 5 # Charging disabled, prevent non-solar charge check interval (seconds) +FAST_POLLING = 1 # Charging enabled, loop delay (seconds) +DELAYED_START_TIME = 30 # When Energy is Available how long do we wait before starting charge (seconds) +DELAYED_STOP_TIME = 90 # When Available Energy is Reduced how long do we wait before stopping charge (seconds) +REPORT_DELAY = 60 # Send status string to MQTT every x (seconds), delayed by as much as SLOW_POLLING_CHK when charging disabled diff --git a/routines.py b/routines.py index 30c6b25..cae9d0e 100644 --- a/routines.py +++ b/routines.py @@ -2,13 +2,14 @@ import sys import subprocess import math +import time import logging -from time import sleep import tomllib from dotenv import load_dotenv from egauge import webapi -import paho.mqtt.client as mqtt from egauge.webapi.device import Register, Local +import paho.mqtt.client as mqtt + # Load parameters from .env load_dotenv() @@ -83,7 +84,7 @@ def verify_new_charge_rate(self, new_charge_rate): if round(self.charge_rate_sensor) >= new_charge_rate: logging.debug(f"New charge rate verified") return True - sleep(0.5) + time.sleep(0.5) logging.debug(f"New charge rate NOT verified") return False @@ -160,6 +161,17 @@ def call_sub_error_handler(cmd): return False return True +def check_elapsed_time(loop_time, compare_time, wait_time): + if compare_time == 0: + compare_time = time.time() # Set counter to current time + return False, compare_time + elif (loop_time - compare_time) >= wait_time: + # Compare current loop time to first time + return True, compare_time + else: + # We haven't waited long enough, keep waiting + return False, compare_time + class MqttCallbacks: """Class to handle MQTT""" From 5eda35fd23ab3cf61ff50d5e37335f3246efdf9f Mon Sep 17 00:00:00 2001 From: sftman18 Date: Tue, 27 Feb 2024 15:44:59 -0600 Subject: [PATCH 2/8] Corrected default back to making a log file --- PVCharge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PVCharge.py b/PVCharge.py index cde8aa1..1adac53 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -10,7 +10,7 @@ config = tomllib.load(fp) logging.basicConfig( - #filename=config["LOG_FILE"], + filename=config["LOG_FILE"], level=logging.INFO, format='%(asctime)s %(levelname)s %(module)s - %(funcName)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', From 07d4a4a9ec0cb3d182027b1756bff8ec69642516 Mon Sep 17 00:00:00 2001 From: sftman18 Date: Wed, 28 Feb 2024 09:35:27 -0600 Subject: [PATCH 3/8] Fixed bug in calculating stopping/starting time message Changed default starting delay to 10 seconds --- PVCharge.py | 4 ++-- example_config.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PVCharge.py b/PVCharge.py index 1adac53..7cfe8b0 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -103,7 +103,7 @@ else: logging.warning(f"Car charging, Available Energy Reduced, charging was NOT successfully stopped") else: - logging.info(f"Car charging, Available Energy Reduced, charging at min rate, stopping in: {config['DELAYED_STOP_TIME'] - loop_time - stop_charging_time} seconds") + logging.info(f"Car charging, Available Energy Reduced, charging at min rate, stopping in: {round(config['DELAYED_STOP_TIME'] - (loop_time - stop_charging_time))} seconds") else: # Car isn't charging, should it be? if Energy.sufficient_generation(config["MIN_CHARGE"]): # If we have enough sun to charge @@ -127,7 +127,7 @@ else: logging.warning(f"Car was NOT woken successfully") else: - logging.info(f"Car is NOT charging, Energy is Available, starting in: {config['DELAYED_START_TIME'] - loop_time - start_charging_time} seconds") + logging.info(f"Car is NOT charging, Energy is Available, starting in: {round(config['DELAYED_START_TIME'] - (loop_time - start_charging_time))} seconds") else: # Car is already charging, set the flag car_is_charging = True diff --git a/example_config.toml b/example_config.toml index b3c9a31..8f57a13 100644 --- a/example_config.toml +++ b/example_config.toml @@ -17,6 +17,6 @@ MAX_CHARGE_LIMIT = 80 # Max charge limit (%) SLOW_POLLING = 30 # Charging disabled, control topic check interval (seconds) SLOW_POLLING_CHK = 5 # Charging disabled, prevent non-solar charge check interval (seconds) FAST_POLLING = 1 # Charging enabled, loop delay (seconds) -DELAYED_START_TIME = 30 # When Energy is Available how long do we wait before starting charge (seconds) +DELAYED_START_TIME = 10 # When Energy is Available how long do we wait before starting charge (seconds) DELAYED_STOP_TIME = 90 # When Available Energy is Reduced how long do we wait before stopping charge (seconds) REPORT_DELAY = 60 # Send status string to MQTT every x (seconds), delayed by as much as SLOW_POLLING_CHK when charging disabled From 75f7637b197c4b79ab1a675d9c51e2b30361c8a1 Mon Sep 17 00:00:00 2001 From: sftman18 Date: Fri, 1 Mar 2024 08:00:23 -0600 Subject: [PATCH 4/8] Change "Slow poll wait" messages to DEBUG as they really aren't that necessary. --- PVCharge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PVCharge.py b/PVCharge.py index 7cfe8b0..bf0a1ae 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -45,7 +45,7 @@ if not charge_tesla: for poll in range(0, config["SLOW_POLLING"]), config["SLOW_POLLING_CHK"]: # While waiting ensure that the car isn't charging if prevent_non_solar_charge: - logging.info("Slow poll wait, ensure car isn't charging") + logging.debug("Slow poll wait, ensure car isn't charging") Energy.sample_sensor() if round(Energy.charge_rate_sensor) >= config["MIN_CHARGE"]: if Car.stop_charging(): # Stop if it is charging From 392663a02be5d2a7cb18d449158ef850d3b5985e Mon Sep 17 00:00:00 2001 From: sftman18 Date: Fri, 1 Mar 2024 08:03:49 -0600 Subject: [PATCH 5/8] Changed 2nd "Slow poll wait" message to DEBUG as it isn't very necessary. --- PVCharge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PVCharge.py b/PVCharge.py index bf0a1ae..7046c38 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -54,7 +54,7 @@ else: logging.warning("Slow poll, Car discovered charging and was NOT stopped successfully") else: - logging.info("Slow poll wait, ignore charging") + logging.debug("Slow poll wait, ignore charging") # Wait configured time before reporting status report_is_due, report_time = routines.check_elapsed_time(loop_time, report_time, config["REPORT_DELAY"]) if report_is_due: From df0eb7c865d1e3526065874a2cf16a6c40c48e89 Mon Sep 17 00:00:00 2001 From: sftman18 Date: Fri, 1 Mar 2024 08:53:38 -0600 Subject: [PATCH 6/8] Moved "Car charging, new rate calculated" messages to DEBUG. Unless the rate changes, there isn't much value in them. --- PVCharge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PVCharge.py b/PVCharge.py index 7046c38..0983b46 100644 --- a/PVCharge.py +++ b/PVCharge.py @@ -73,7 +73,7 @@ # Use math.floor() on calculate_charge_rate to ensure we are always just "under" the available PV generation capacity # Use round() on charge_rate_sensor to prevent constant requests when on the edge of a value new_charge_rate = math.floor(Energy.calculate_charge_rate(new_sample=False)) - logging.info(f"Car charging, new rate calculated: {new_charge_rate}, current rate: {round(Energy.charge_rate_sensor)}") + logging.debug(f"Car charging, new rate calculated: {new_charge_rate}, current rate: {round(Energy.charge_rate_sensor)}") if new_charge_rate != round(Energy.charge_rate_sensor): # Set new charge rate if Car.set_charge_rate(new_charge_rate): From b7e9808afdb2266b392d56048fc9a96db2e4dea5 Mon Sep 17 00:00:00 2001 From: sftman18 Date: Fri, 1 Mar 2024 09:45:30 -0600 Subject: [PATCH 7/8] Added PREVENT_NON_SOLAR_CHARGE to config.toml to allow the default to be changed. --- example_config.toml | 5 +++-- routines.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/example_config.toml b/example_config.toml index 8f57a13..0e8b546 100644 --- a/example_config.toml +++ b/example_config.toml @@ -1,6 +1,7 @@ # Configuration file -LOG_FILE = 'PVCharge.log' -LOG_LEVEL = "INFO" # Default INFO, change to DEBUG to diagnose issues +LOG_FILE = 'PVCharge.log' # Log file name to use +LOG_LEVEL = "INFO" # Default INFO, change to DEBUG to diagnose issues +PREVENT_NON_SOLAR_CHARGE = "True" # Default for after-hours charging, unless changed via MQTT # MQTT Control topics TOPIC_PREVENT_NON_SOLAR_CHARGE = "topic_base/prevent_non_solar_charge" diff --git a/routines.py b/routines.py index cae9d0e..f061edf 100644 --- a/routines.py +++ b/routines.py @@ -185,7 +185,10 @@ def __init__(self): self.topic_teslamate_plugged_in = config["TOPIC_TESLAMATE_PLUGGED_IN"] self.topic_teslamate_battery_level = config["TOPIC_TESLAMATE_BATTERY_LEVEL"] self.max_charge_limit = config["MAX_CHARGE_LIMIT"] - self.var_topic_prevent_non_solar_charge = False + if config["PREVENT_NON_SOLAR_CHARGE"] == "True": + self.var_topic_prevent_non_solar_charge = True + else: + self.var_topic_prevent_non_solar_charge = False self.var_topic_teslamate_geofence = False self.var_topic_teslamate_plugged_in = False self.var_topic_teslamate_battery_level = 0 From 2fa900bde5ad12b068869ac9db9a4b147859c30f Mon Sep 17 00:00:00 2001 From: sftman18 Date: Fri, 1 Mar 2024 09:52:16 -0600 Subject: [PATCH 8/8] Adjusted example_config.toml to reflect the previous default state of False (hard coded in routines.py) for prevent_non_solar_charge --- example_config.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_config.toml b/example_config.toml index 0e8b546..41a3121 100644 --- a/example_config.toml +++ b/example_config.toml @@ -1,7 +1,7 @@ # Configuration file -LOG_FILE = 'PVCharge.log' # Log file name to use -LOG_LEVEL = "INFO" # Default INFO, change to DEBUG to diagnose issues -PREVENT_NON_SOLAR_CHARGE = "True" # Default for after-hours charging, unless changed via MQTT +LOG_FILE = 'PVCharge.log' # Log file name to use +LOG_LEVEL = "INFO" # Default INFO, change to DEBUG to diagnose issues +PREVENT_NON_SOLAR_CHARGE = "False" # Default for after-hours charging, unless changed via MQTT # MQTT Control topics TOPIC_PREVENT_NON_SOLAR_CHARGE = "topic_base/prevent_non_solar_charge"