car_heater.py 3.5 KB

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