# # CAR HEATER - One-minute loop, using template sensor for preheat time # from datetime import datetime, timedelta @time_trigger("cron(* * * * *)") # run every minute def car_heater_minute_job(): # Static variable to track one-time schedule state across calls if not hasattr(car_heater_minute_job, 'old_once_active'): car_heater_minute_job.old_once_active = False now = datetime.now() # # 1. READ PREHEAT TIME FROM TEMPLATE SENSOR # preheat_raw = state.get("sensor.car_heater_preheat_time") try: preheat = int(float(preheat_raw)) except (ValueError, TypeError): preheat = 60 # safe fallback log.error(f"Car heater: preheat sensor returned invalid value '{preheat_raw}'") after_heat = 30 # minutes after departure to keep heater on # # 2. DETERMINE IF HEATER SHOULD BE ON # should_on = False # Force ON overrides everything if state.get("input_boolean.heater_force_on") == "on": should_on = True # Helper function def in_window(dt_entity, active_entity): """Return True if current time is inside the heating window.""" if state.get(active_entity) != "on": return False tstr = state.get(dt_entity) if not tstr: return False try: dep_time = datetime.strptime(tstr, "%H:%M:%S").time() except (ValueError, TypeError): log.error(f"Car heater: invalid datetime '{tstr}' for {dt_entity}") return False departure = datetime.combine(now.date(), dep_time) start = departure - timedelta(minutes=preheat) stop = departure + timedelta(minutes=after_heat) return start <= now <= stop # # WEEKDAY SCHEDULES (Mon–Fri only) # if now.weekday() <= 4: if in_window("input_datetime.heater_time1", "input_boolean.heater_time1_active"): should_on = True if in_window("input_datetime.heater_time2", "input_boolean.heater_time2_active"): should_on = True if in_window("input_datetime.heater_time3", "input_boolean.heater_time3_active"): should_on = True if in_window("input_datetime.heater_time4", "input_boolean.heater_time4_active"): should_on = True # # ONE-TIME SCHEDULE (any day) # once_in_window = in_window("input_datetime.heater_once_time", "input_boolean.heater_once_active") if once_in_window: should_on = True car_heater_minute_job.old_once_active = True # # 3. WRITE VIRTUAL SENSOR # state.set("binary_sensor.car_heater_should_be_on", "on" if should_on else "off") # # 4. CONTROL THE REAL SHELLY RELAY # current_state = state.get("switch.shelly_car_heater") desired_state = "on" if should_on else "off" if desired_state != current_state: action = "turn_on" if should_on else "turn_off" log.info(f"Car heater: switching relay to {action}") service.call("switch", action, entity_id="switch.shelly_car_heater") # # 5. RESET ONE-TIME DEPARTURE AFTER WINDOW ENDS # if state.get("input_boolean.heater_once_active") == "on": # If we were in the window last cycle but not anymore, reset the boolean if car_heater_minute_job.old_once_active and not once_in_window: log.info("Car heater: resetting one-time schedule (window ended)") service.call("input_boolean", "turn_off", entity_id="input_boolean.heater_once_active") car_heater_minute_job.old_once_active = False else: # Boolean is off, reset tracking variable car_heater_minute_job.old_once_active = False