Last active
June 21, 2023 12:56
-
-
Save joe248/4c35be8001981eee7cd016cf05b97c42 to your computer and use it in GitHub Desktop.
Home Assistant AppDaemon App to reduce energy usage during ComEd peak load events
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Helpful links: | |
# https://www.pjm.com/-/media/planning/res-adeq/load-forecast/20181017-summer-2018-peaks-and-5cps.ashx?la=en | |
# https://www.comed.com/SiteCollectionDocuments/MyAccount/MyService/ComEd_5_Peaks.pdf | |
# https://www.citizensutilityboard.org/blog/2019/01/23/comeds-hourly-pricing-how-to-calculate-the-customer-capacity-charge/ | |
# https://secure.comed.com/MyAccount/MyService/Pages/UsageDataTool.aspx | |
# NOTE: This was originally written for a Nest thermostat, hence the use of the term 'Eco mode'. Instead of turning the thermostat off during times of high load or high price, I would just switch it into Eco mode. I no longer have a Nest so I now just switch my thermostat off, but I still refer to this as Eco mode in the script. I use the ECO_ACTIVATED_DUE_TO_PRICE input_boolean in Home Assistant as a flag to know whether or not 'Eco mode' was activated due to this script vs. someone manually turning off the thermostat. | |
import appdaemon.plugins.hass.hassapi as hass | |
import datetime | |
import sqlalchemy | |
import random | |
# The load ratio is the ratio of the current load to the previous peak load. If the current load ratio doesn't meet this threshold the script doesn't take action | |
LOAD_RATIO_THRESHOLD = 0.99 | |
# The script won't take action if the load is less than the minimum | |
PJM_COMED_MINIMUM_LOAD = 17500 | |
PJM_TOTAL_MINIMUM_LOAD = 130000 | |
# The hourly rate of change of the load is compared against these values. When the peak occurs, the rate of change will go from positive to negative, but we want to catch it before the peak occurs so we can reduce our usage. If you set the rates too high you'll err on the side of caution and may end up reducing your usage (i.e. not running your air conditioner when it's really hot out) for several hours before the actual peak event. If you set the rates too low you may end up not predicting the peak event before it occurs and therefore not reducing your usage when you needed to. | |
PJM_COMED_LOAD_RATE_PER_HOUR = 350 | |
PJM_TOTAL_LOAD_RATE_PER_HOUR = 2200 | |
PJM_COMED_1H_3H_THRESHOLD = 0.50 | |
PJM_TOTAL_1H_3H_THRESHOLD = 0.66667 | |
DB_URL = 'postgresql://@/homeassistant' | |
NOTIFY_SERVICE = 'notify/mobile_app_joes_iphone_12_pro' | |
ECO_ACTIVATED_DUE_TO_PRICE = 'input_boolean.eco_activated_due_to_price' | |
SAVED_THERMOSTAT_MODE = 'input_select.saved_thermostat_mode' | |
THERMOSTAT = 'climate.downstairs_thermostat' | |
COMED_CURRENT_HOUR_AVERAGE_PRICE = 'sensor.comed_total_current_hour_average_price' | |
COMED_FIVE_MINUTE_PRICE = 'sensor.comed_total_5_minute_price' | |
COMED_PRICE_THRESHOLD = 'input_number.comed_price_threshold' | |
PJM_COMED_INSTANTANEOUS_LOAD = 'sensor.pjm_instantaneous_zone_load_comed' | |
PJM_COMED_LOAD_CURRENT_HOUR_HIGH = 'sensor.pjm_comed_load_current_hour_high' | |
PJM_COMED_LOAD_HIGH_MARKER = 'sensor.pjm_comed_load_high_marker' | |
PJM_TOTAL_INSTANTANEOUS_LOAD = 'sensor.pjm_instantaneous_total_load' | |
PJM_TOTAL_LOAD_CURRENT_HOUR_HIGH = 'sensor.pjm_total_load_current_hour_high' | |
PJM_TOTAL_LOAD_HIGH_MARKER = 'sensor.pjm_total_load_high_marker' | |
class ElectricityCostManager(hass.Hass): | |
def initialize(self): | |
from sqlalchemy.orm import sessionmaker, scoped_session | |
try: | |
engine = sqlalchemy.create_engine(DB_URL) | |
self.sessionmaker = scoped_session(sessionmaker(bind=engine)) | |
# run a dummy query just to test the db_url | |
sess = self.sessionmaker() | |
sess.execute("SELECT 1;") | |
except sqlalchemy.exc.SQLAlchemyError as err: | |
_LOGGER.error("Couldn't connect using %s DB_URL: %s", DB_URL, err) | |
return | |
# Flag stating whether or not we are currently updating | |
self.is_updating = False | |
# Store the last time we updated | |
self.last_update_time = datetime.datetime.fromtimestamp(0) | |
# Store the reason for entering Eco mode | |
self.last_reason = None | |
# Store the last time that we changed the thermostat state | |
self.last_thermostat_state_change_time = None | |
# Listen for ComEd hourly average price change | |
self.listen_state(self.comed_price_change_callback, entity_id=COMED_CURRENT_HOUR_AVERAGE_PRICE) | |
# Listen for PJM regional ComEd load high marker, current hour high, and instantaneous load changes | |
self.listen_state(self.pjm_comed_load_current_hour_high_callback, | |
entity_id=PJM_COMED_LOAD_CURRENT_HOUR_HIGH) | |
self.listen_state(self.pjm_comed_load_high_marker_callback, | |
entity_id=PJM_COMED_LOAD_HIGH_MARKER) | |
self.listen_state(self.pjm_comed_instantaneous_load_callback, | |
entity_id=PJM_COMED_INSTANTANEOUS_LOAD) | |
# Listen for PJM total load high marker, current hour high, and instantaneous load changes | |
self.listen_state(self.pjm_total_load_current_hour_high_callback, | |
entity_id=PJM_TOTAL_LOAD_CURRENT_HOUR_HIGH) | |
self.listen_state(self.pjm_total_load_high_marker_callback, | |
entity_id=PJM_TOTAL_LOAD_HIGH_MARKER) | |
self.listen_state(self.pjm_total_instantaneous_load_callback, | |
entity_id=PJM_TOTAL_INSTANTANEOUS_LOAD) | |
# Listen for ComEd price threshold slider changes | |
self.listen_state(self.comed_price_threshold_callback, | |
entity_id=COMED_PRICE_THRESHOLD) | |
self.update_state() | |
def comed_price_change_callback(self, entity, attribute, old, new, kwargs): | |
#self.log("ComEd current hour average price changed to {}".format(new)) | |
price_threshold = float(self.get_state(COMED_PRICE_THRESHOLD)) | |
if float(old) <= price_threshold and float(new) > price_threshold: | |
self.log("ComEd hourly price went over threshold from {} to {}".format(old, new)) | |
self.update_state() | |
elif float(old) > price_threshold and float(new) <= price_threshold: | |
self.log("ComEd hourly price went below threshold from {} to {}".format(old, new)) | |
self.update_state() | |
def pjm_comed_load_current_hour_high_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM ComEd current hour high load changed to {}".format(new)) | |
self.update_state() | |
def pjm_comed_load_high_marker_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM ComEd all-summer high load marker changed to {}".format(new)) | |
self.update_state() | |
def pjm_comed_instantaneous_load_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM ComEd instantaneous load changed to {}".format(new)) | |
self.update_state() | |
def pjm_total_load_current_hour_high_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM total current hour high load changed to {}".format(new)) | |
self.update_state() | |
def pjm_total_load_high_marker_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM total all-summer high load marker changed to {}".format(new)) | |
self.update_state() | |
def pjm_total_instantaneous_load_callback(self, entity, attribute, old, new, kwargs): | |
self.log("PJM total instantaneous load changed to {}".format(new)) | |
self.update_state() | |
def comed_price_threshold_callback(self, entity, attribute, old, new, kwargs): | |
self.log("ComEd price threshold changed to {}".format(new)) | |
comed_hourly_price = float(self.get_state(COMED_CURRENT_HOUR_AVERAGE_PRICE)) | |
if float(old) < comed_hourly_price and float(new) >= comed_hourly_price: | |
self.log("ComEd price threshold {} increased above hourly price {}".format(new, comed_hourly_price)) | |
self.update_state() | |
elif float(old) >= comed_hourly_price and float(new) < comed_hourly_price: | |
self.log("ComEd price threshold {} decreased below hourly price {}".format(new, comed_hourly_price)) | |
self.update_state() | |
def update_state(self): | |
#self.log("Update state") | |
# Determine the last time we updated, in case something happens and our is_updating flag doesn't get reset, use a watchdog of 60 seconds to ensure we keep updating | |
current_time = datetime.datetime.now() | |
time_delta = current_time - self.last_update_time | |
if (not self.is_updating) or (time_delta.seconds > 60): | |
# Some events, such as the PJM total and ComEd load values changing, fire two events very quickly. We want to delay slightly before processing so that the second value has updated before we attempt to read it | |
self.is_updating = True | |
# Delay for a random amount of time so that two threads trying to update at the same time won't do so | |
delay_time = random.randint(1, 4) | |
self.log("Delaying for {} seconds...".format(delay_time)) | |
self.run_in(self.do_update_state, delay_time) | |
def do_update_state(self, kwargs): | |
self.log("Running") | |
eco_activated_due_to_price = self.get_state(ECO_ACTIVATED_DUE_TO_PRICE) | |
saved_thermostat_mode = self.get_state(SAVED_THERMOSTAT_MODE) | |
thermostat_current_operation_mode = self.get_state(THERMOSTAT) | |
comed_hourly_price = float(self.get_state(COMED_CURRENT_HOUR_AVERAGE_PRICE)) | |
comed_five_minute_price = float(self.get_state(COMED_FIVE_MINUTE_PRICE)) | |
price_threshold = float(self.get_state(COMED_PRICE_THRESHOLD)) | |
pjm_comed_load_current_hour_high = int(self.get_state(PJM_COMED_LOAD_CURRENT_HOUR_HIGH)) | |
pjm_comed_load_last_hour_high = int(self.get_entity_previous_hour_high(PJM_COMED_LOAD_CURRENT_HOUR_HIGH)) | |
pjm_comed_load_high_marker = int(self.get_state(PJM_COMED_LOAD_HIGH_MARKER)) | |
pjm_total_load_current_hour_high = int(self.get_state(PJM_TOTAL_LOAD_CURRENT_HOUR_HIGH)) | |
pjm_total_load_last_hour_high = int(self.get_entity_previous_hour_high(PJM_TOTAL_LOAD_CURRENT_HOUR_HIGH)) | |
pjm_total_load_high_marker = int(self.get_state(PJM_TOTAL_LOAD_HIGH_MARKER)) | |
# Calculate the current PJM and ComEd loads as a percentage of our high load markers | |
pjm_comed_load_ratio = float(pjm_comed_load_current_hour_high) / float(pjm_comed_load_high_marker) | |
pjm_total_load_ratio = float(pjm_total_load_current_hour_high) / float(pjm_total_load_high_marker) | |
# We add 5 seconds to each interval because of the fact that we have a random delay when we update, when we're looking for the old value to calculate the derivative | |
pjm_total_load_rate_of_change = self.get_entity_rate_of_change(PJM_TOTAL_INSTANTANEOUS_LOAD, '1 hour 5 second') | |
pjm_comed_load_rate_of_change = self.get_entity_rate_of_change(PJM_COMED_INSTANTANEOUS_LOAD, '1 hour 5 second') | |
pjm_total_load_rate_of_change_15_minutes = self.get_entity_rate_of_change(PJM_TOTAL_INSTANTANEOUS_LOAD, '15 minute 5 second') | |
pjm_comed_load_rate_of_change_15_minutes = self.get_entity_rate_of_change(PJM_COMED_INSTANTANEOUS_LOAD, '15 minute 5 second') | |
pjm_total_load_rate_of_change_3_hours = self.get_entity_rate_of_change(PJM_TOTAL_INSTANTANEOUS_LOAD, '3 hour 5 second') | |
pjm_comed_load_rate_of_change_3_hours = self.get_entity_rate_of_change(PJM_COMED_INSTANTANEOUS_LOAD, '3 hour 5 second') | |
pjm_total_1h_3h_rate = pjm_total_load_rate_of_change / pjm_total_load_rate_of_change_3_hours | |
pjm_comed_1h_3h_rate = pjm_comed_load_rate_of_change / pjm_comed_load_rate_of_change_3_hours | |
pjm_total_15m_1h_rate = pjm_total_load_rate_of_change_15_minutes / pjm_total_load_rate_of_change | |
pjm_comed_15m_1h_rate = pjm_comed_load_rate_of_change_15_minutes / pjm_comed_load_rate_of_change | |
# We only need to worry about PJM and ComEd load during the summer months (June 1 - Sept 30) | |
month = datetime.datetime.today().month | |
is_summer = month >= 6 and month <= 9 | |
if is_summer: | |
# Log Load Rates of Change (LROC) | |
self.log("PJM LROC: 3h: {}, 1h: {}, 15m: {}".format(pjm_total_load_rate_of_change_3_hours, pjm_total_load_rate_of_change, pjm_total_load_rate_of_change_15_minutes)) | |
#self.log("PJM 1h/3h: {}".format(pjm_total_1h_3h_rate)) | |
#self.log("PJM 15m/1h: {}".format(pjm_total_15m_1h_rate)) | |
self.log("ComEd LROC: 3h: {}, 1h: {}, 15m: {}".format(pjm_comed_load_rate_of_change_3_hours, pjm_comed_load_rate_of_change, pjm_comed_load_rate_of_change_15_minutes)) | |
#self.log("ComEd 1h/3h: {}".format(pjm_comed_1h_3h_rate)) | |
#self.log("ComEd 15m/1h: {}".format(pjm_comed_15m_1h_rate)) | |
self.log("PJM total load last hour high = {}, ComEd load last hour high = {}".format(pjm_total_load_last_hour_high, pjm_comed_load_last_hour_high)) | |
self.log("Updating state. eco_activated_due_to_price = {}, saved_thermostat_mode = {}, thermostat_current_operation_mode = {}, comed_load_ratio = {}, pjm_total_load_ratio = {}, comed_hourly_price = {}".format(eco_activated_due_to_price, saved_thermostat_mode, thermostat_current_operation_mode, pjm_comed_load_ratio, pjm_total_load_ratio, comed_hourly_price)) | |
eco_required = False | |
reason = None # If reason is set we'll send a notification | |
if thermostat_current_operation_mode != 'heat' and thermostat_current_operation_mode != 'cool' and not eco_activated_due_to_price: | |
# Only activate Eco mode if we're currently heating or cooling, or we're already in Eco mode | |
self.log("Passing") | |
pass | |
elif comed_hourly_price > price_threshold: | |
# The current hourly price is over our threshold | |
eco_required = True | |
reason = "high energy price" | |
elif (pjm_comed_load_current_hour_high > PJM_COMED_MINIMUM_LOAD and \ | |
is_summer and \ | |
( \ | |
# If the current load is higher than the marker we always activate eco | |
# Actually, don't do this. The actual peak for the day might not occur for | |
# a few more hours so we don't want to activate Eco until we know we're close to peak | |
#(pjm_comed_load_current_hour_high >= pjm_comed_load_high_marker) \ | |
#or \ | |
# We are approaching the load marker, on the way up, and over the ratio threshold | |
( | |
pjm_comed_load_rate_of_change < PJM_COMED_LOAD_RATE_PER_HOUR \ | |
#(pjm_comed_1h_3h_rate < PJM_COMED_1H_3H_THRESHOLD) \ | |
#or pjm_comed_15m_1h_rate < 0.6) \ | |
and pjm_comed_load_ratio > LOAD_RATIO_THRESHOLD \ | |
and (pjm_comed_load_current_hour_high >= pjm_comed_load_last_hour_high \ | |
or pjm_comed_load_rate_of_change > -80) \ | |
) \ | |
) | |
): | |
eco_required = True | |
reason = "high ComEd energy load event" | |
elif (pjm_total_load_current_hour_high > PJM_TOTAL_MINIMUM_LOAD and \ | |
is_summer and \ | |
( \ | |
# If the current load is higher than the marker we always activate eco | |
# Actually, don't do this. The actual peak for the day might not occur for | |
# a few more hours so we don't want to activate Eco until we know we're close to peak | |
#(pjm_total_load_current_hour_high >= pjm_total_load_high_marker) \ | |
#or \ | |
# We are approaching the load marker, on the way up, and over the ratio threshold | |
( | |
pjm_total_load_rate_of_change < PJM_TOTAL_LOAD_RATE_PER_HOUR \ | |
#(pjm_total_1h_3h_rate < PJM_TOTAL_1H_3H_THRESHOLD \ | |
# or pjm_total_15m_1h_rate < 1.0) \ | |
and pjm_total_load_ratio > LOAD_RATIO_THRESHOLD \ | |
and (pjm_total_load_current_hour_high >= pjm_total_load_last_hour_high \ | |
or pjm_total_load_rate_of_change > -100) \ | |
) \ | |
) | |
): | |
eco_required = True | |
reason = "high PJM energy load event" | |
else: | |
# Conditions state we shouldn't use Eco | |
if thermostat_current_operation_mode.lower() == 'off' and comed_five_minute_price > price_threshold: | |
# The ComEd five minute price is still higher than our threshold so don't deactivate Eco yet | |
eco_required = True | |
if eco_required: | |
#self.log("Would activate eco") | |
self.activate_eco(thermostat_current_operation_mode, reason) | |
else: | |
self.deactivate_eco(thermostat_current_operation_mode) | |
self.is_updating = False | |
self.last_update_time = datetime.datetime.now() | |
def activate_eco(self, thermostat_current_operation_mode, reason): | |
if thermostat_current_operation_mode.lower() == 'off': | |
self.log("Already in Eco mode") | |
return | |
eco_activated_due_to_price = self.get_state(ECO_ACTIVATED_DUE_TO_PRICE) | |
# Only activate if our boolean is set to off. This handles the case where we activate Eco, | |
# then someone manually deactivates Eco using the thermostat. We don't want to then | |
# immediately reactivate it. We wait until the boolean gets reset after the price | |
# decreases below the threshold | |
if eco_activated_due_to_price == 'on': | |
self.log("Not activating Eco mode because eco_activated_due_to_price already on") | |
return | |
# Sometimes multiple triggers cause us to update state several times within a short period. | |
# We don't want to send multiple commands to the thermostat so we use a buffer period of | |
# a few seconds | |
if self.last_thermostat_state_change_time: | |
current_time = datetime.datetime.now() | |
time_delta = current_time - self.last_thermostat_state_change_time | |
if time_delta.seconds < 5: | |
self.log("Only {} seconds since we last activated Eco. Doing nothing".format(time_delta.seconds)) | |
return | |
self.log("Activating Eco mode") | |
if reason: | |
# Send a notification | |
message = "Activating Eco due to " + reason | |
self.call_service(NOTIFY_SERVICE, message = message) | |
# Store the reason, even if it's None | |
self.last_reason = reason | |
# Store the current thermostat mode so we can recall it later | |
self.set_state(SAVED_THERMOSTAT_MODE, state=thermostat_current_operation_mode) | |
# Set our boolean so we know that we activated Eco due to price | |
self.set_state(ECO_ACTIVATED_DUE_TO_PRICE, state='on') | |
# Now set the thermostat to Eco mode | |
self.call_service("climate/set_hvac_mode", entity_id=THERMOSTAT, hvac_mode='off') | |
# Store the time that we changed the thermostat state | |
self.last_thermostat_state_change_time = datetime.datetime.now() | |
def deactivate_eco(self, thermostat_current_operation_mode): | |
if thermostat_current_operation_mode.lower() != 'off': | |
#self.log("Not in Eco mode so not deactivating. Resetting eco_activated_due_to_price boolean.") | |
# This handles the case where Eco was automatically activated but then someone manually | |
# deactivated it. Once the price drops and this script attempts to deactivate Eco, it | |
# will do nothing because Eco was already deactivated manually, however, we want to | |
# reset our boolean so that the next high price event will trigger Eco mode again | |
self.set_state(ECO_ACTIVATED_DUE_TO_PRICE, state='off') | |
return | |
eco_activated_due_to_price = self.get_state(ECO_ACTIVATED_DUE_TO_PRICE) | |
if (eco_activated_due_to_price != 'on'): | |
self.log("Eco mode wasn't activated by us so not deactivating") | |
return | |
# Sometimes multiple triggers cause us to update state several times within a short period. | |
# We don't want to send multiple commands to the thermostat so we use a buffer period of | |
# a few seconds | |
if self.last_thermostat_state_change_time: | |
current_time = datetime.datetime.now() | |
time_delta = current_time - self.last_thermostat_state_change_time | |
if time_delta.seconds < 5: | |
self.log("Only {} seconds since we last deactivated Eco. Doing nothing".format(time_delta.seconds)) | |
return | |
self.log("Deactivating Eco mode") | |
if self.last_reason: | |
# Only send a deactivate message if we sent an activate message | |
self.call_service(NOTIFY_SERVICE, message = "Deactivated Eco mode") | |
self.last_reason = None | |
# Clear our boolean | |
self.set_state(ECO_ACTIVATED_DUE_TO_PRICE, state='off') | |
# Restore the thermostat to the operation mode it was in when we initiated eco mode | |
saved_thermostat_mode = self.get_state(SAVED_THERMOSTAT_MODE) | |
self.call_service("climate/set_hvac_mode", entity_id=THERMOSTAT, hvac_mode=saved_thermostat_mode) | |
# Store the time that we changed the thermostat state | |
self.last_thermostat_state_change_time = datetime.datetime.now() | |
# Reset our saved thermostat mode variables | |
self.set_state(SAVED_THERMOSTAT_MODE, state='none') | |
# This gets the derivative of entity_id over interval | |
def get_entity_rate_of_change(self, entity_id, interval): | |
try: | |
sess = self.sessionmaker() | |
query = "with t1 as (\ | |
select cast(state as integer) as state, \ | |
last_updated_ts as last_updated_epoch, \ | |
to_timestamp(last_updated_ts) as last_updated \ | |
from states \ | |
where metadata_id = (select metadata_id from states_meta \ | |
where entity_id = :entity_id) \ | |
and state != 'unknown' and state != '' \ | |
order by last_updated_ts desc limit 1 \ | |
), t2 as (\ | |
select cast(state as integer) as state, \ | |
last_updated_ts as last_updated_epoch, \ | |
to_timestamp(last_updated_ts) as last_updated \ | |
from states \ | |
where metadata_id = (select metadata_id from states_meta \ | |
where entity_id = :entity_id) \ | |
and state != 'unknown' and state != '' \ | |
and to_timestamp(last_updated_ts) <= (now() - interval :interval) \ | |
order by last_updated_ts desc limit 1 \ | |
) \ | |
select (t1.state - t2.state) as rate, \ | |
t1.state as t1state, t2.state as t2state, \ | |
t1.last_updated as t1lastupdated, t2.last_updated as t2lastupdated, \ | |
(t1.last_updated_epoch - t2.last_updated_epoch) as time_difference \ | |
from t1 inner join t2 on \ | |
t1.state >= t2.state or t1.state <= t2.state;" | |
result = sess.execute(query, {'entity_id': entity_id, 'interval': interval}) | |
if not result.returns_rows or result.rowcount == 0: | |
#self.log("query returned no results") | |
return None | |
for res in result: | |
rate = res['rate'] | |
time_difference = res['time_difference'] | |
hourly_rate = (rate / time_difference) * 3600 | |
#t1state = res['t1state'] | |
#t2state = res['t2state'] | |
#t1lastupdated = res['t1lastupdated'] | |
#t2lastupdated = res['t2lastupdated'] | |
#self.log("t1state = {}, t2state = {}. t1lastupdated = {}. t2lastupdated = {}".format(t1state, t2state, t1lastupdated, t2lastupdated)) | |
return int(round(hourly_rate)) | |
except sqlalchemy.exc.SQLAlchemyError as err: | |
self.log("Error executing query: {}".format(err)) | |
return None | |
finally: | |
sess.close() | |
# This gets the higest value for entity_id during the previous hour | |
def get_entity_previous_hour_high(self, entity_id): | |
try: | |
sess = self.sessionmaker() | |
query = "select max(cast(state as integer)) as state \ | |
from states \ | |
where metadata_id = (select metadata_id from states_meta \ | |
where entity_id = :entity_id) \ | |
and state != 'unknown' and state != '' \ | |
and last_updated_ts >= extract (epoch from date_trunc('hour', now() - interval '1 hour')) \ | |
and last_updated_ts < extract (epoch from date_trunc('hour', now()));" | |
result = sess.execute(query, {'entity_id': entity_id}) | |
if not result.returns_rows or result.rowcount == 0: | |
#self.log("query returned no results") | |
return None | |
for res in result: | |
return res['state'] | |
except sqlalchemy.exc.SQLAlchemyError as err: | |
self.log("Error executing query: {}".format(err)) | |
return None | |
finally: | |
sess.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment