car_heater.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #
  2. # CAR HEATER - One-minute loop, using template sensor for preheat time
  3. #
  4. from datetime import datetime, timedelta
  5. @time_trigger("cron(* * * * *)") # run every minute
  6. def car_heater_minute_job():
  7. # Static variable to track one-time schedule state across calls
  8. if not hasattr(car_heater_minute_job, 'old_once_active'):
  9. car_heater_minute_job.old_once_active = False
  10. now = datetime.now()
  11. #
  12. # 1. READ PREHEAT TIME FROM TEMPLATE SENSOR
  13. #
  14. preheat_raw = state.get("sensor.car_heater_preheat_time")
  15. try:
  16. preheat = int(float(preheat_raw))
  17. except (ValueError, TypeError):
  18. preheat = 60 # safe fallback
  19. log.error(f"Car heater: preheat sensor returned invalid value '{preheat_raw}'")
  20. after_heat = 30 # minutes after departure to keep heater on
  21. #
  22. # 2. DETERMINE IF HEATER SHOULD BE ON
  23. #
  24. should_on = False
  25. # Force ON overrides everything
  26. if state.get("input_boolean.heater_force_on") == "on":
  27. should_on = True
  28. # Helper function
  29. def in_window(dt_entity, active_entity):
  30. """Return True if current time is inside the heating window."""
  31. if state.get(active_entity) != "on":
  32. return False
  33. tstr = state.get(dt_entity)
  34. if not tstr:
  35. return False
  36. try:
  37. dep_time = datetime.strptime(tstr, "%H:%M:%S").time()
  38. except (ValueError, TypeError):
  39. log.error(f"Car heater: invalid datetime '{tstr}' for {dt_entity}")
  40. return False
  41. departure = datetime.combine(now.date(), dep_time)
  42. start = departure - timedelta(minutes=preheat)
  43. stop = departure + timedelta(minutes=after_heat)
  44. return start <= now <= stop
  45. #
  46. # WEEKDAY SCHEDULES (Mon–Fri only)
  47. #
  48. if now.weekday() <= 4:
  49. if in_window("input_datetime.heater_time1", "input_boolean.heater_time1_active"):
  50. should_on = True
  51. if in_window("input_datetime.heater_time2", "input_boolean.heater_time2_active"):
  52. should_on = True
  53. if in_window("input_datetime.heater_time3", "input_boolean.heater_time3_active"):
  54. should_on = True
  55. if in_window("input_datetime.heater_time4", "input_boolean.heater_time4_active"):
  56. should_on = True
  57. #
  58. # ONE-TIME SCHEDULE (any day)
  59. #
  60. once_in_window = in_window("input_datetime.heater_once_time", "input_boolean.heater_once_active")
  61. if once_in_window:
  62. should_on = True
  63. car_heater_minute_job.old_once_active = True
  64. #
  65. # 3. WRITE VIRTUAL SENSOR
  66. #
  67. state.set("binary_sensor.car_heater_should_be_on",
  68. "on" if should_on else "off")
  69. #
  70. # 4. CONTROL THE REAL SHELLY RELAY
  71. #
  72. current_state = state.get("switch.shelly_car_heater")
  73. desired_state = "on" if should_on else "off"
  74. if desired_state != current_state:
  75. action = "turn_on" if should_on else "turn_off"
  76. log.info(f"Car heater: switching relay to {action}")
  77. service.call("switch", action, entity_id="switch.shelly_car_heater")
  78. #
  79. # 5. RESET ONE-TIME DEPARTURE AFTER WINDOW ENDS
  80. #
  81. if state.get("input_boolean.heater_once_active") == "on":
  82. # If we were in the window last cycle but not anymore, reset the boolean
  83. if car_heater_minute_job.old_once_active and not once_in_window:
  84. log.info("Car heater: resetting one-time schedule (window ended)")
  85. service.call("input_boolean", "turn_off",
  86. entity_id="input_boolean.heater_once_active")
  87. car_heater_minute_job.old_once_active = False
  88. else:
  89. # Boolean is off, reset tracking variable
  90. car_heater_minute_job.old_once_active = False