Skip to content

Instantly share code, notes, and snippets.

@ALERTua
Created September 7, 2023 11:00
Show Gist options
  • Save ALERTua/0e89f5279dd0e1de7ee270c9e718d8d2 to your computer and use it in GitHub Desktop.
Save ALERTua/0e89f5279dd0e1de7ee270c9e718d8d2 to your computer and use it in GitHub Desktop.
auto_ac
# https://github.com/custom-components/pyscript
# https://github.com/ALERTua/ha_pyscript_modules
from imports import *
DEFAULT_BOOST_TEMP_DIFFERENCE = 1.2
DEFAULT_TEMP_TOLERANCE_UP = 0.5
DEFAULT_TEMP_TOLERANCE_DOWN = 0.5
DEFAULT_HOLD = constants.HOLD_1M
PRECISION = 0.5
MIN_TEMP = 18
MAX_TEMP = 32
PRESET_MODE_BOOST = 'boost'
PRESET_MODE_OFF = 'none'
HVAC_MODE_COOL = 'cool'
HVAC_MODE_HEAT = 'heat'
HVAC_MODE_FAN = 'fan_only'
HVAC_MODE_OFF = 'off'
FAN_MODE_AUTO = 'Auto'
FAN_MODES = ['Silence', '1', '2', '3', '4', '5']
OFFICE_AC = constants.OFFICE_AC
OFFICE_IB = 'input_boolean.office_auto_ac'
OFFICE_TEMP = constants.OFFICE_TEMPERATURE
OFFICE_WANTED_TEMP = 'input_number.office_wanted_temperature'
OFFICE_ALLOWED_MODES = 'input_select.office_auto_ac_allowed_modes'
OFFICE_TOLERANCE_UP = 0.2
OFFICE_TOLERANCE_DOWN = 0.2
OFFICE_KWARGS = dict(
ac_entity=OFFICE_AC,
cur_temp_entity=OFFICE_TEMP,
allowed_modes_selector=OFFICE_ALLOWED_MODES,
wanted_temperature_entity=OFFICE_WANTED_TEMP,
boost_trigger_difference=DEFAULT_BOOST_TEMP_DIFFERENCE,
tolerance_up=OFFICE_TOLERANCE_UP,
tolerance_down=OFFICE_TOLERANCE_DOWN,
change_temperature=True,
change_fan_speed=True,
fan_speed_limit=None,
allow_turning_off=False,
)
ROOM_AC = constants.ROOM_AC
ROOM_IB = 'input_boolean.room_auto_ac'
ROOM_TEMP = constants.ROOM_TEMPERATURE
ROOM_WANTED_TEMP = 'input_number.room_wanted_temperature'
ROOM_ALLOWED_MODES = 'input_select.room_auto_ac_allowed_modes'
ROOM_TOLERANCE_UP = 0.5
ROOM_TOLERANCE_DOWN = 0.5
ROOM_KWARGS = dict(
ac_entity=constants.ROOM_AC,
cur_temp_entity=ROOM_TEMP,
allowed_modes_selector=ROOM_ALLOWED_MODES,
wanted_temperature_entity=ROOM_WANTED_TEMP,
boost_trigger_difference=DEFAULT_BOOST_TEMP_DIFFERENCE,
tolerance_up=ROOM_TOLERANCE_UP,
tolerance_down=ROOM_TOLERANCE_DOWN,
change_temperature=True,
change_fan_speed=True,
fan_speed_limit=None,
allow_turning_off=True,
)
KITCHEN_AC = constants.KITCHEN_AC
KITCHEN_IB = 'input_boolean.kitchen_auto_ac'
KITCHEN_TEMP = constants.KITCHEN_TEMPERATURE
KITCHEN_WANTED_TEMP = 'input_number.kitchen_wanted_temperature'
KITCHEN_ALLOWED_MODES = 'input_select.kitchen_auto_ac_allowed_modes'
KITCHEN_TOLERANCE_UP = 0.5
KITCHEN_TOLERANCE_DOWN = 0.5
KITCHEN_KWARGS = dict(
ac_entity=KITCHEN_AC,
cur_temp_entity=KITCHEN_TEMP,
allowed_modes_selector=KITCHEN_ALLOWED_MODES,
wanted_temperature_entity=KITCHEN_WANTED_TEMP,
boost_trigger_difference=DEFAULT_BOOST_TEMP_DIFFERENCE,
tolerance_up=KITCHEN_TOLERANCE_UP,
tolerance_down=KITCHEN_TOLERANCE_DOWN,
change_temperature=True,
change_fan_speed=True,
fan_speed_limit=None,
allow_turning_off=True,
)
BEDROOM_AC = constants.BEDROOM_AC
BEDROOM_IB = constants.BEDROOM_AUTO_AC
BEDROOM_TEMP = constants.BEDROOM_TEMPERATURE
BEDROOM_WANTED_TEMP = 'input_number.bedroom_wanted_temperature'
BEDROOM_ALLOWED_MODES = 'input_select.bedroom_auto_ac_allowed_modes'
BEDROOM_TOLERANCE_UP = 0.5
BEDROOM_TOLERANCE_DOWN = 0.5
BEDROOM_HOLD = constants.HOLD_5M
BEDROOM_KWARGS = dict(
ac_entity=BEDROOM_AC,
cur_temp_entity=BEDROOM_TEMP,
allowed_modes_selector=BEDROOM_ALLOWED_MODES,
wanted_temperature_entity=BEDROOM_WANTED_TEMP,
boost_trigger_difference=DEFAULT_BOOST_TEMP_DIFFERENCE,
tolerance_up=BEDROOM_TOLERANCE_UP,
tolerance_down=BEDROOM_TOLERANCE_DOWN,
change_temperature=True,
change_fan_speed=True,
fan_speed_limit=None,
allow_turning_off=False,
)
@state_trigger(
BEDROOM_TEMP,
state_hold=BEDROOM_HOLD,
kwargs=BEDROOM_KWARGS,
)
@state_trigger(
BEDROOM_IB,
BEDROOM_WANTED_TEMP,
BEDROOM_ALLOWED_MODES,
state_hold=3,
kwargs=BEDROOM_KWARGS,
)
@state_active(f"{BEDROOM_IB} == 'on'"
f" and {BEDROOM_AC} not in {constants.UNK_S} "
f" and {BEDROOM_TEMP} not in {constants.UNK_S} "
f" and {BEDROOM_WANTED_TEMP} not in {constants.UNK_S} "
f" and {BEDROOM_ALLOWED_MODES} not in {constants.UNK_S}")
def auto_ac_bedroom(trigger_type=None, var_name=None, value=None, old_value=None, context=None, **kwargs):
ib = entity(BEDROOM_IB)
if ib.state() != 'on':
return
return _auto_ac_main(trigger_type=trigger_type, var_name=var_name, value=value, old_value=old_value, context=context, **kwargs)
@state_trigger(
OFFICE_TEMP,
state_hold=DEFAULT_HOLD,
kwargs=OFFICE_KWARGS,
)
@state_trigger(
OFFICE_IB,
OFFICE_WANTED_TEMP,
OFFICE_ALLOWED_MODES,
state_hold=2,
kwargs=OFFICE_KWARGS,
)
@state_active(f"{OFFICE_IB} == 'on'"
f" and {OFFICE_AC} not in {constants.UNK_S} "
f" and {OFFICE_TEMP} not in {constants.UNK_S} "
f" and {OFFICE_WANTED_TEMP} not in {constants.UNK_S} "
f" and {OFFICE_ALLOWED_MODES} not in {constants.UNK_S}")
def auto_ac_office(trigger_type=None, var_name=None, value=None, old_value=None, context=None, **kwargs):
ib = entity(OFFICE_IB)
if ib.state() != 'on':
return
return _auto_ac_main(trigger_type=trigger_type, var_name=var_name, value=value, old_value=old_value,
context=context, **kwargs)
@state_trigger(
ROOM_TEMP,
state_hold=DEFAULT_HOLD,
kwargs=ROOM_KWARGS,
)
@state_trigger(
ROOM_ALLOWED_MODES,
ROOM_IB,
ROOM_WANTED_TEMP,
state_hold=2,
kwargs=ROOM_KWARGS,
)
@state_active(f"{ROOM_IB} == 'on'"
f" and {ROOM_AC} not in {constants.UNK_S}"
f" and {ROOM_TEMP} not in {constants.UNK_S}"
f" and {ROOM_WANTED_TEMP} not in {constants.UNK_S}"
f" and {ROOM_ALLOWED_MODES} not in {constants.UNK_S}")
def auto_ac_room(trigger_type=None, var_name=None, value=None, old_value=None, context=None, **kwargs):
ib = entity(ROOM_IB)
if ib.state() != 'on':
return
return _auto_ac_main(trigger_type=trigger_type, var_name=var_name, value=value, old_value=old_value, context=context, **kwargs)
@state_trigger(
KITCHEN_TEMP,
state_hold=DEFAULT_HOLD,
kwargs=KITCHEN_KWARGS,
)
@state_trigger(
KITCHEN_IB,
KITCHEN_WANTED_TEMP,
KITCHEN_ALLOWED_MODES,
state_hold=3,
kwargs=KITCHEN_KWARGS,
)
@state_active(f"{KITCHEN_IB} == 'on'"
f" and {KITCHEN_AC} not in {constants.UNK_S}"
f" and {KITCHEN_TEMP} not in {constants.UNK_S}"
f" and {KITCHEN_WANTED_TEMP} not in {constants.UNK_S}"
f" and {KITCHEN_ALLOWED_MODES} not in {constants.UNK_S}")
def auto_ac_kitchen(trigger_type=None, var_name=None, value=None, old_value=None, context=None, **kwargs):
ib = entity(KITCHEN_IB)
if ib.state() != 'on':
return
return _auto_ac_main(trigger_type=trigger_type, var_name=var_name, value=value, old_value=old_value, context=context, **kwargs)
def turn_off(ac_entity, allow_turning_off=True, ac_action_wait=3):
if allow_turning_off:
ac_entity.turn_off()
task.sleep(ac_action_wait)
ac_entity.turn_off()
task.sleep(ac_action_wait)
else:
ac_entity.set_hvac_mode(HVAC_MODE_FAN)
task.sleep(ac_action_wait)
ac_entity.set_preset_mode(PRESET_MODE_OFF)
task.sleep(ac_action_wait)
ac_entity.set_fan_mode(FAN_MODES[0])
def _auto_ac_main(trigger_type=None, var_name=None, value=None, old_value=None, context=None, **kwargs):
# 'entity_id': 'climate.ac_office',
# 'state': 'cool',
# 'attributes': {
# 'hvac_modes': [
# <HVACMode.FAN_ONLY: 'fan_only'>,
# <HVACMode.DRY: 'dry'>,
# <HVACMode.COOL: 'cool'>,
# <HVACMode.HEAT: 'heat'>,
# <HVACMode.HEAT_COOL: 'heat_cool'>,
# <HVACMode.OFF: 'off'>
# ],
# 'min_temp': 7,
# 'max_temp': 35,
# 'target_temp_step': 1,
# 'fan_modes': ['Auto', 'Silence', '1', '2', '3', '4', '5'],
# 'preset_modes': ['none', 'away', 'eco', 'boost'],
# 'swing_modes': ['Off', 'Vertical', 'Horizontal', '3D'],
# 'current_temperature': 24.0,
# 'temperature': 24.0,
# 'fan_mode': 'Silence',
# 'hvac_action': <HVACAction.COOLING: 'cooling'>,
# 'preset_mode': 'none',
# 'swing_mode': 'Off',
# 'friendly_name': 'OfficeAC',
# 'supported_features': <ClimateEntityFeature.SWING_MODE|PRESET_MODE|FAN_MODE|TARGET_TEMPERATURE: 57>},
# 'last_changed': '2023-05-23T16:32:31.787600+00:00',
# 'last_updated': '2023-05-23T16:32:31.787600+00:00',
wanted_temp_entity_id = kwargs.get('wanted_temperature_entity')
ac_entity_id = kwargs.get('ac_entity')
assert ac_entity_id, f"ac_entity_id: {ac_entity_id}, kwargs: {kwargs}"
tolerance_up = round(float(kwargs.get('tolerance_up', DEFAULT_TEMP_TOLERANCE_UP)), 1)
tolerance_up = max(tolerance_up, PRECISION)
tolerance_down = round(float(kwargs.get('tolerance_down', DEFAULT_TEMP_TOLERANCE_DOWN)), 1)
tolerance_down = max(tolerance_down, PRECISION)
cur_temp_entity_id = kwargs.get('cur_temp_entity')
change_temperature = kwargs.get('change_temperature', True)
change_fan_speed = kwargs.get('change_fan_speed', True)
boost_temp_difference = round(float(kwargs.get('boost_trigger_difference', DEFAULT_BOOST_TEMP_DIFFERENCE)), 1)
allowed_modes_selector = kwargs.get('allowed_modes_selector', None)
fan_speed_limit = kwargs.get('fan_speed_limit', None)
allow_turning_off = kwargs.get('allow_turning_off', True)
cur_temp_entity = entity(cur_temp_entity_id)
cur_temp = round(float(cur_temp_entity.state()), 1)
cur_temp_friendly_name = cur_temp_entity.friendly_name()
ac_entity = entity(ac_entity_id)
ac_state = ac_entity.state()
ac_preset_mode = ac_entity.attrs().get('preset_mode')
ac_fan_speed = ac_entity.attrs().get('fan_mode')
ac_temperature = round(float(ac_entity.attrs().get('temperature', cur_temp) or cur_temp), 1)
ac_friendly_name = ac_entity.friendly_name()
ac_action_wait = 3
ac_inside_temp = float(ac_entity.attrs().get('current_temperature', cur_temp) or cur_temp)
msgs = DiscordMsgBucket(name=f"{__name__} for {ac_friendly_name}", target='1111696430206287892')
wanted_temp_entity = entity(wanted_temp_entity_id)
wanted_temp_entity_friendly_name = wanted_temp_entity.friendly_name()
wanted_temp = round(float(wanted_temp_entity.state()), 1)
temp_low_bar = wanted_temp - tolerance_down
msgs.add(f'↓:{wanted_temp}-{tolerance_down}:{temp_low_bar}')
temp_high_bar = wanted_temp + tolerance_up
msgs.add(f'↑:{wanted_temp}+{tolerance_down}:{temp_high_bar}')
temp_difference = round(float(cur_temp - wanted_temp), 1)
# log.debug(f"temp_difference = round(float(cur_temp {cur_temp} - wanted_temp {wanted_temp}), 1) = {temp_difference}")
temp_difference_abs = abs(temp_difference)
# log.debug(f"temp_difference_abs={temp_difference_abs}")
allowed_modes = [HVAC_MODE_COOL, HVAC_MODE_HEAT]
if allowed_modes_selector:
allowed_modes_selector_entity = entity(allowed_modes_selector)
allowed_mode = allowed_modes_selector_entity.state()
allowed_modes = [_ for _ in allowed_modes if _ in allowed_mode.lower()]
allowed_modes.append(HVAC_MODE_FAN)
allowed_modes.append(HVAC_MODE_OFF)
wanted_state = HVAC_MODE_OFF
preset_target = PRESET_MODE_OFF
if cur_temp >= temp_high_bar:
msgs.add(f'{cur_temp} >= ↑{temp_high_bar}')
wanted_state = HVAC_MODE_COOL
elif cur_temp <= temp_low_bar: # got it
msgs.add(f'{cur_temp} <= ↓{temp_low_bar}')
wanted_state = HVAC_MODE_HEAT
else:
wanted_state = ac_state
msgs.add(f'{temp_high_bar} ↑ {cur_temp} ↓ {temp_low_bar}')
if wanted_state not in allowed_modes:
msgs.add(f'{ac_friendly_name} wanted_state unallowed: {wanted_state}. Turning off.')
msgs.send()
turn_off(ac_entity, allow_turning_off, ac_action_wait)
return
elif ac_state != wanted_state:
msgs.add(f"wanted_state: {wanted_state}")
if ac_state != HVAC_MODE_OFF and wanted_state == HVAC_MODE_OFF:
log.debug(f"{ac_friendly_name} wanted_state off. Turning off.")
msgs.send()
turn_off(ac_entity, allow_turning_off, ac_action_wait)
return
elif ac_state == HVAC_MODE_OFF and wanted_state == HVAC_MODE_OFF:
log.debug(f"{ac_friendly_name} ac_state == wanted_state == {HVAC_MODE_OFF}")
return
if ac_state != HVAC_MODE_OFF and ac_state not in allowed_modes:
msgs.add(f'{ac_friendly_name} current state unallowed: {ac_state}. Turning off.')
msgs.send()
turn_off(ac_entity, allow_turning_off, ac_action_wait)
return
# target_temperature = wanted_temp - temp_difference
target_temperature = ac_inside_temp - temp_difference # base target temperature on AC inside temperature
target_temperature_max = target_temperature + tolerance_up
target_temperature_min = target_temperature - tolerance_down
target_temperature = max(target_temperature, MIN_TEMP)
target_temperature_max = max(target_temperature_max, MIN_TEMP)
target_temperature_min = max(target_temperature_min, MIN_TEMP)
target_temperature = min(target_temperature, MAX_TEMP)
target_temperature_max = min(target_temperature_max, MAX_TEMP)
target_temperature_min = min(target_temperature_min, MAX_TEMP)
try:
index_try = FAN_MODES.index(ac_fan_speed)
except:
index_try = 'Unknown'
msgs_init = [
f":leaves: {__name__} for {ac_friendly_name}:",
f"🌡️ {cur_temp_friendly_name}: {cur_temp}",
f"🎯 {wanted_temp_entity_friendly_name}: {wanted_temp}",
f'temp_difference: {temp_difference}',
f"state: {ac_state}",
f"allowed_modes: {allowed_modes}",
f"preset_mode: {ac_preset_mode}",
f"🌬️fan_speed: {ac_fan_speed}: {index_try}/{len(FAN_MODES)}",
]
if ac_state != wanted_state and wanted_state in allowed_modes:
msgs.add(f'Setting HVAC state {ac_state} to {wanted_state}')
ac_entity.set_hvac_mode(wanted_state)
task.sleep(ac_action_wait)
if change_fan_speed: # and temp_difference_ok
wanted_fan_speed = float(len(FAN_MODES)) * (temp_difference or 0.1) / float(boost_temp_difference)
wanted_fan_speed -= 1 # indexes from 0
# log.debug(f"before mod: {wanted_fan_speed}")
# if wanted_fan_speed % 1.0 >= 0.41:
if (wanted_fan_speed_div := wanted_fan_speed % 1.0) >= 0.5:
wanted_fan_speed -= wanted_fan_speed_div
wanted_fan_speed += 1
# log.debug(f"after mod: {wanted_fan_speed}")
wanted_fan_speed = int(round(wanted_fan_speed, 0))
# log.debug(f"after int: {wanted_fan_speed}")
if fan_speed_limit is not None:
wanted_fan_speed = min(wanted_fan_speed, int(fan_speed_limit))
# log.debug(f"after min: {wanted_fan_speed}")
if wanted_fan_speed > len(FAN_MODES) - 1:
preset_target = PRESET_MODE_BOOST
else:
wanted_fan_speed = max(min(wanted_fan_speed, len(FAN_MODES) - 1), 0)
# log.debug(f"after max: {wanted_fan_speed}")
# log.debug(f"""{ac_friendly_name}
# float(len(FAN_MODES)) * (temp_difference or 0.1) / float(boost_temp_difference):
# {float(len(FAN_MODES))} * {(temp_difference or 0.1)} / {float(boost_temp_difference)}
# wanted_fan_speed: {wanted_fan_speed}/{len(FAN_MODES)}
# """)
try:
wanted_fan_speed = FAN_MODES[wanted_fan_speed]
except Exception as e:
tools.telegram_message(f"error wanted_fan_speed: {wanted_fan_speed} of {FAN_MODES} {type(e)} {e}")
wanted_fan_speed = FAN_MODES[0]
if ac_fan_speed != wanted_fan_speed:
msgs.add(f'Setting fan speed {ac_fan_speed} to {wanted_fan_speed}/{len(FAN_MODES)}')
ac_entity.set_fan_mode(wanted_fan_speed)
task.sleep(ac_action_wait)
if ac_preset_mode != preset_target:
msgs.add(f'Setting preset mode {ac_preset_mode} to {preset_target}')
ac_entity.set_preset_mode(preset_target)
task.sleep(ac_action_wait)
if change_temperature and ac_temperature != target_temperature and temp_difference_abs > 0:
msgs.add(f'Setting {ac_friendly_name} temperature {ac_temperature} to '
f'{target_temperature_min}-{target_temperature}-{target_temperature_max}')
ac_entity.set_temperature(hvac_mode=wanted_state, temperature=target_temperature,
target_temp_high=target_temperature_max, target_temp_low=target_temperature_min)
task.sleep(ac_action_wait)
if msgs.msgs:
msgs.msgs = msgs_init + msgs.msgs
msgs.send()
else:
log.debug(f"{__name__}: nothing to do for {ac_friendly_name}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment