| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- #
- # 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():
- 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 = 25 # 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)
- #
- if in_window("input_datetime.heater_once_time", "input_boolean.heater_once_active"):
- should_on = 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 IF WINDOW PASSED
- #
- if state.get("input_boolean.heater_once_active") == "on":
- tstr = state.get("input_datetime.heater_once_time")
- if tstr:
- try:
- dep_time = datetime.strptime(tstr, "%H:%M:%S").time()
- except (ValueError, TypeError):
- log.error(f"Car heater: failed to parse heater_once_time '{tstr}'")
- return
- departure = datetime.combine(now.date(), dep_time)
- stop = departure + timedelta(minutes=after_heat)
- if now > stop:
- log.info("Car heater: resetting one-time schedule")
- service.call("input_boolean", "turn_off",
- entity_id="input_boolean.heater_once_active")
|