Chicken coop winterization help: water heater

I used to run a Raspberry Pi to control some relays for a heater for the watering can as well as some lights to extend the “daylight” for the hens to promote laying. I’m in the process of switching to Home Assistant and ESP Home to do the same + run some heating bulbs in the coop. I’ve got several smart plugs (Kauf EP12’s) and an ESP board with some Dallas DS18b20’s. The heater bulbs seem straightforward enough to use a simple climate control. After some learning, I converted my python implementation for supplemental daylight to Home Assistant (described below). However, I could use some pointers on progressing with my water heater implementation.

I use a very typical galvanized water can for the chickens. And I purchased an inexpensive resistive heating mat (automotive engine oil pan heater) to sit under the watering can. A DS18b20 monitors air temperature near the watering can and I empirically determined how long to cycle the heater on to keep the water can from freezing. I like this approach because I don’t have to put anything but water in the container, which makes for easy cleaning. The python script is run in cron every 15 minutes, and a relay was cycled on for several minutes, inversely proportional to air temperature, and then off for the remainder of the 15 minute block of time:

#!/usr/bin/python2
#This code is intended to run with cron, I believe it should execute every 15 minutes

import os
import glob
import time
import datetime
import RPi.GPIO as GPIO

GPIO_PIN = 11

GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_PIN, GPIO.OUT)

#Enable the linux modules to read the one-wire temp devices
os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

#uncomment for autodetect
#base_dir = '/sys/bus/w1/devices/'
#device_folder = [glob.glob(base_dir + '28*')[0]]#,glob.glob(base_dir + '28*')[1],glob.glob(base_dir + '28*')[2]]
#device_file = [device_folder[0] + '/w1_slave']#, device_folder[1] + '/w1_slave', device_folder[2] + '/w1_slave']

device_coop = '/sys/bus/w1/devices/######/w1_slave'

def read_temp_raw(device):
    """Pass this function either 0 or 1 to get the raw data from the
        Temperature Sensor"""
    f = open(device, 'r')
    lines = f.readlines()
    f.close()
    return lines

def read_temp(device):
    """Pass 0 or 1 to get the temperature in celcius of either sensor"""
    lines = read_temp_raw(device) #missing "device"
    while lines[0].strip()[-3:] != 'YES':
        time.sleep(0.2)
        lines = read_temp_raw(device)
    equals_pos = lines[1].find('t=')
    if equals_pos != -1:
        temp_string = lines[1][equals_pos+2:]
        temp_c = float(temp_string) / 1000.0
        return temp_c


while True:
	try:
		if (read_temp(device_coop) <= -18):
			GPIO.output(GPIO_PIN,1)
			print "It is sub zero!"
			time.sleep(420) #7 minutes out of 15
			GPIO.output(GPIO_PIN, 0)
			break
		elif (read_temp(device_coop) <= -4):
			GPIO.output(GPIO_PIN, 1)
			print "It is really cold"
			time.sleep(300) #5 minutes out of 15
			GPIO.output(GPIO_PIN, 0)
			break
		elif (read_temp(device_coop) <= 0):
			GPIO.output(GPIO_PIN, 1)
			print "It is a little cold"
			time.sleep(120) #2 min out of 15
			GPIO.output(GPIO_PIN, 0)
			break
		else:
			GPIO.output(GPIO_PIN, 0)
			print "It is warm enough"
			break
	#Stop on Ctrl-C and clean up GPIO pins
	except KeyboardInterrupt:
		GPIO.cleanup()#!/usr/bin/python

So my question is, what is a sensible way to structure similar control in Home Assistant / ESP Home? Thanks for any suggestions.

And for my supplemental lighting...

My python script from the Raspberry Pi that was checked by cron at 3 AM every day:

#!/usr/bin/python2

import time
import datetime
import RPi.GPIO as GPIO
import ephem

GPIO.setmode(GPIO.BCM)
GPIO.setup(10, GPIO.OUT, initial=GPIO.LOW) 
#GPIO.output(10, 0)
light_hours = 13

sun = ephem.Sun()
home = ephem.Observer()
home.lat, home.lon = ######################
sun.compute(home)
sunrise, sunset = (ephem.localtime(home.next_rising(sun)),ephem.localtime(home.next_setting(sun)))
daylight_t = sunset - sunrise
daylight_f = (ephem.Date(sunset) - ephem.Date(sunrise))
timer_f = (ephem.Date(0 + light_hours*ephem.hour) - daylight_f)
time_on_t = ephem.Date(ephem.Date(sunrise) - timer_f)

while True:
	try:
		if (ephem.Date(datetime.datetime.now())) < time_on_t:
			time.sleep(300)
                elif (ephem.Date(datetime.datetime.now())) > ephem.Date(ephem.Date(sunrise)):
                        GPIO.cleanup()#!/usr/bin/python
                        print "The sun should have risen by now"
                        break
		elif (ephem.Date(datetime.datetime.now())) >= time_on_t: 
			GPIO.output(10, 1)
			print "Lights are being turned on now"
			time.sleep(300)
	#Stop on Ctrl-C and clean up GPIO pins
	except KeyboardInterrupt:
		GPIO.cleanup()#!/usr/bin/python	

Based on the Sun2 add-on, I created a helper, templates, and an automation:

#input_numbers.yaml
t_chicken_daylight_hours:
  name: "Minimum hours of daylight hens"
  initial: 13
  min: 10
  max: 18
  step: 0.05
  mode: box
#templates.yaml
sensor:
  - name: "Chicken daylight light turn ON time"
    unique_id: "chicken_daylight_light_turn_on_time"
    device_class: timestamp
    state: >
      {% set sunrise = states('sensor.sun_rising') | as_datetime | as_local %}
      {% set daylight_needed = timedelta(hours=(states.input_number.t_chicken_daylight_hours.state | float)) %}
      {% set daylight_actual = timedelta(hours=(states('sensor.sun_daylight') | float)) %}
      {% set turn_on_time = sunrise - daylight_needed + daylight_actual %}
      {{ turn_on_time }}

  - name: "Chicken daylight duration deficit"
    unique_id: "chicken_daylight_deficit"
    state: >
      {% set daylight_needed = states.input_number.t_chicken_daylight_hours.state | float %}
      {% set daylight_actual = states('sensor.sun_daylight') | float %}
      {% set defecit = daylight_needed - daylight_actual %}
      {{ defecit | round(4) }}

  - name: "Chicken daylight light turn OFF time"
    unique_id: "chicken_daylight_light_turn_off_time"
    device_class: timestamp
    state: >
      {% set sunrise = states('sensor.sun_rising') | as_datetime | as_local %}
      {% set wait_time = timedelta(hours=0.5) %}
      {% set turn_off_time = sunrise + wait_time %}
      {{ turn_off_time}}
#automations.yaml
- id: ####
  alias: Chicken Light Turn ON
  description: ''
  triggers:
  - trigger: time
    at: sensor.chicken_daylight_light_turn_on_time
  conditions:
  - condition: numeric_state
    entity_id: sensor.chicken_daylight_duration_deficit
    above: 0
  - condition: time
    before: sensor.chicken_daylight_light_turn_off_time
  actions:
  - action: switch.turn_on
    metadata: {}
    data: {}
    target:
      device_id: ####
  mode: single
- id: ####
  alias: Chicken Light Turn OFF
  description: ''
  triggers:
  - trigger: time
    at: sensor.chicken_daylight_light_turn_off_time
  conditions:
  - condition: numeric_state
    entity_id: sensor.chicken_daylight_duration_deficit
    above: 0
  actions:
  - action: switch.turn_off
    metadata: {}
    data: {}
    target:
      device_id: ####
  mode: single

If you want to keep it as simple, you could use:
Gpio switch component

Temperature sensor

and Time or Interval component
https://esphome.io/components/interval.html

Then just make some simple automation:

interval:
  - interval: 15min
    then:
      - switch.turn_on: heater_relay
      - delay: !lambda 'return id(temp_sensor).state;' # add your math for temp/time conversion
      - switch.turn_off: heater_relay

You could do it also without interval component if you set temp sensor update interval as your trigger and do the automation within Switch component…

Or you can use more sophisticated climate, thermostat, PID etc. components.

I’m probably not going to get a direct replacement with GPIO Switch, since I was planning to use the Kauf smart plug, but Interval could get me the start I was looking for - thanks!

I was able to solve my watering can heater control using the Automation UI. The interval was handled with time_pattern trigger. Much simpler than I was making it out to be.

id: '1731000609249'
alias: Chicken Water Can Heater
description: ''
triggers:
  - trigger: time_pattern
    minutes: /15
conditions: []
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.chicken_coop_run_temperature
            below: 0
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
          - delay:
              hours: 0
              minutes: 7
              seconds: 0
              milliseconds: 0
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
      - conditions:
          - condition: numeric_state
            entity_id: sensor.chicken_coop_run_temperature
            below: 25
            above: 0
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
          - delay:
              hours: 0
              minutes: 5
              seconds: 0
              milliseconds: 0
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
      - conditions:
          - condition: numeric_state
            entity_id: sensor.chicken_coop_run_temperature
            below: 32
            above: 25
        sequence:
          - action: switch.turn_on
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
          - delay:
              hours: 0
              minutes: 2
              seconds: 0
              milliseconds: 0
          - action: switch.turn_off
            metadata: {}
            data: {}
            target:
              device_id: 30a5b9790ce523d5d719dece03698f0e
    default:
      - action: switch.turn_off
        metadata: {}
        data: {}
        target:
          device_id: 30a5b9790ce523d5d719dece03698f0e
mode: single