Ljusstyrning ver #3

Ljuset är uppdelat på sju reläer (då mitt reläkort har åtta portar och en används till värmaren):

#1 på GPIO2 (pinne 3): 2 st Royal Blue (445nm), används som månljus men är även tända med de andra för växternas skull.
#2 på GPIO3 (pinne 5): 1 st röd (660nm) och 1 st vit (6500K)
#3 på GPIO17 (pinne 11): 1 st röd (660nm) och 1 st vit (6500K)
#4 på GPIO27 (pinne 13): 1 st vit (6500K)
#5 på GPIO10 (pinne 19): 1 st vit (6500K)
#6 på GPIO9 (pinne 21): 1 st vit (6500K)
#7 på GPIO11 (pinne 23): 1 st vit (6500K)

Dags för lite styrning! Först går månljuset på, sedan kommer en soluppgång. I slutet på dagen kommer en solnedgång som avslutas med en stunds månljus.

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import datetime
import syslog
import smtplib
from time import strftime
 
syslog.openlog('aqua-light')
syslog.syslog('Aqua-light started')
 
# Definiera tider
moon_on = datetime.datetime(2020, 1, 1, 10, 00)
sunrise_on = datetime.datetime(2020, 1, 1, 10, 30)
sunset_on = datetime.datetime(2020, 1, 1, 21, 00)
moon_off = datetime.datetime(2020, 1, 1, 22, 00)
 
# Klasser
class sunClass:
    def __init__(self):
        self.sun = [3,17,27,10,9,11]
        self.sunLength = len(self.sun)
    def init(self):
        syslog.syslog('sunClass.init called')
        for x in range (0, self.sunLength):
                GPIO.setup(self.sun[x], GPIO.OUT)
    def off(self):
        syslog.syslog('sunClass.off called')
        for x in range (0, self.sunLength):
            GPIO.output(self.sun[x], LEDOff)
    def sunrise(self, runtime):
        syslog.syslog('Sunrise started')
        for x in range (0, self.sunLength):
            GPIO.output(self.sun[x], LEDOn)
            if x != self.sunLength - 1: time.sleep(runtime/(self.sunLength-1))
        syslog.syslog('Sunrise ended')
    def sunset(self, runtime):
        syslog.syslog('Sunset started')
        for x in range (self.sunLength-1,-1,-1):
            GPIO.output(self.sun[x], LEDOff)
            if x != 0: time.sleep(runtime/(self.sunLength-1))
        syslog.syslog('Sunset ended')
 
class moonClass:
    def __init__(self):
        self.moon = 2
    def init(self):
        syslog.syslog('moonClass.init called')
        GPIO.setup(self.moon, GPIO.OUT)
    def off(self):
        syslog.syslog('moonClass.off called')
        GPIO.output(self.moon, LEDOff)  
    def on(self):
        syslog.syslog('moonClass.on called')
        GPIO.output(self.moon, LEDOn)   
         
# Variabler
sun = sunClass()
moon = moonClass()
LEDOn = 0
LEDOff = 1
 
# Funktioner
             
# Initiera GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
sun.init()
moon.init()
syslog.syslog('GPIO initialized')
 
# Initiera state
now = datetime.datetime.now()
sun.off()
moon.off()
state = 0
if now.time() > moon_on.time(): state = 1
if now.time() > sunrise_on.time(): state = 2
if now.time() > sunset_on.time(): state = 3
if now.time() > moon_off.time(): state = 0
if state == 1 or state == 3:
    moon.on()
elif state != 0:
    moon.on()
    sun.sunrise(10)
syslog.syslog('Initial state is %d' % state)
 
# Huvudloop
while True:
    now = datetime.datetime.now()
    if state == 0:
        # Night mode, wait for morning moon
        if now.time() > moon_on.time() and now.time() < moon_off.time():
            state = 1
            syslog.syslog('Entering state 1')
            moon.on()
    elif state == 1:
        # Morning moon, wait for sunrise
        if now.time() > sunrise_on.time():
            state = 2
            syslog.syslog('Entering state 2')
            sun.sunrise(1800)
    elif state == 2:
        # Daytime, wait for sunset
        if now.time() > sunset_on.time():
            state = 3
            syslog.syslog('Entering state 3')
            sun.sunset(1800)
    elif state == 3:
        # evening moon, wait for night 
        if now.time() > moon_off.time(): 
            state = 0
            syslog.syslog('Entering state 0')
            moon.off()
    time.sleep(60)

För att skriptet ska starta automatiskt behöver man skapa filen aqua-light.service i /lib/systemd/system med följande innehåll:

[Unit]
Description=Aqua-Light
After=multi-user.target

[Service]
Type=idle
ExecStart=/usr/local/bin/aquacontrol/aqua-light.py

[Install]
WantedBy=multi-user.target

Kör sedan

sudo chmod +x /usr/local/bin/aquacontrol/aqua-light.py 
sudo chmod 644 /lib/systemd/system/aqua-light.service 
sudo systemctl daemon-reload 
sudo systemctl enable aqua-light.service

så ska det starta automatiskt vid omstart. Det är praktiskt att få loggen som skapas till en egen fil. Skapa filen /etc/rsyslog.d/aqua-light.conf med innehållet

if $programname == 'aqua-light' then /var/log/aqua-light.log
Share

Repetition av temperatur (igen)

Hur man mäter temperatur och hur jag använder det för att styra ett relä som slår av och på värmaren är beskrivet på flera andra ställen i bloggen men här kommer en sammanställning och uppdatering.

Jag aktiverade 1-wire i raspi-config under uppsättningen av min paj och har anslutit min DS18B20. Man behöver ett pull-up-motstånd på 4,7kΩ mellan plus- och data-kablarna för att kunna ansluta den direkt till GPIO på pajen. Data är anslutet till standardporten GPIO4 (pinne 7) och minus till valfri jord-pinne, jag använder nummer 25. Plus ansluter man normalt till en av 3,3V-pinnarna men jag har anslutit den till GPIO5 (pinne 29) för att kunna slå av och på strömmen via programmet. Det händer att DS18B20 slutar svarar (känt problem, tydligen vanligare på billiga kopior) och på det här sättet kan jag starta om den när det händer.

Reläet som styr värmaren är anslutet till GPIO22 (pinne 15).

För att kunna presentera data och statistik installerade jag webbservern Apache tillsammans med MRTG som kan presentera historiken grafiskt:

sudo apt install apache2 mrtg

För att MRTG ska få data från temperatur-sensorn krävs det en liten fil som jag kallar mrtg.py. Man får byta adressen till filen w1_slave till aktuell adress, varje sensor har sin egen:

#!/usr/bin/python
from time import strftime

tfile = open("/sys/bus/w1/devices/28-011553897dff/w1_slave", 'r')
text = tfile.read()
tfile.close()

secondline = text.split("\n")[1]
temperaturedata = secondline.split(" ")[9]
temperature = float(temperaturedata[2:])
temperature = int(temperature / 10)

print(str(temperature))
print("0")
print(strftime("%Y-%m-%d %H:%M:%S"))
print("Temperatur i kar 3")

Man behöver även byta ut /etc/mrtg.cfg mot en som hämtar data på rätt ställe och visar det som man vill:

######################################################################
# Multi Router Traffic Grapher -- Sample Configuration File
######################################################################
# This file is for use with mrtg-2.5.4c

# Global configuration
WorkDir: /var/www/html/mrtg
WriteExpires: Yes
RunAsDaemon: Yes
Interval: 5

Title[^]: Temperaturstatistik for 

Title[kar3]: kar 3
PageTop[kar3]: <h1>Temperaturen i kar 3</h1>
Target[kar3]: `/usr/local/bin/aquacontrol/mrtg.py`
MaxBytes[kar3]: 4000
Options[kar3]: growright,gauge,expscale
Factor[kar3]: 0.01
YLegend[kar3]: Temperatur
YTicsFactor[kar3]: 0.01
ShortLegend[kar3]: C
Legend1[kar3]: Temperatur i grader celcius
Legend2[kar3]: 
Legend3[kar3]: Maximal 5 Minute Incoming Traffic
Legend4[kar3]: Maximal 5 Minute Outgoing Traffic
LegendI[kar3]:  Temp:
LegendO[kar3]: 

För att MRTG ska starta automatiskt behövs ett startscript, skapa filen mrtg i  /etc/init.d med följande innehåll:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          mrtg
# Required-Start:    
# Required-Stop:     
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: mrtg init script
# Description:       This file is used to start, stop, restart, 
#					 and determined status of the mrtg daemon.
# Author: 			 iceflatline <iceflatline@gmail.com>
### END INIT INFO
 
### START OF SCRIPT
set -e
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="mrtg"
NAME=mrtg
DAEMON=/usr/bin/$NAME
DAEMON_ARGS="/etc/mrtg.cfg"
PIDFILE=/etc/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
 
# Exit if the mrtg package is not installed
[ -x "$DAEMON" ] || exit 0
 
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
 
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
 
# Function that starts the mrtg daemon
start()
{
	env LANG=C start-stop-daemon --start --quiet \
	--exec $DAEMON -- $DAEMON_ARGS
}
 
# Function that stops the mrtg daemon
stop()
{
	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \
	--pidfile $PIDFILE 
}
 
case "$1" in
  start)
	log_daemon_msg "Starting $DESC" 
	start
	case "$?" in
		0) log_end_msg 0 ;;
		1) log_end_msg 1 ;;
	esac
	;;
  stop)
	log_daemon_msg "Stopping $DESC"
	stop
	case "$?" in
		0) log_end_msg 0 ;;
		1) log_end_msg 1 ;;
	esac
	;;
  restart|force-reload)
	log_daemon_msg "Restarting $DESC" 
	stop
	case "$?" in
	  0|1)
		start
		case "$?" in
			0) log_end_msg 0 ;;
			1) log_end_msg 1 ;; 
		esac
		;;
	esac
	;;
	status)
    status_of_proc "$DAEMON" "$NAME"  
    ;;
  *)
	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" 
	;;
esac
exit 0
### END OF SCRIPT

Kör sedan

sudo chmod +x /usr/local/bin/aquacontrol/mrtg.py 
sudo chmod +x /etc/init.d/mrtg 
sudo systemctl daemon-reload 
sudo update-rc.d mrtg defaults
sudo mkdir /var/www/html/mrtg

så ska MRTG-biten vara klar. Dags för lite styrning! Följande Python-skript läser av temperaturen varje minut, kollar mot gränsvärden, slår av och på värmaren och larmar via epost om det avviker för mycket. Ange dina egna epostuppgifter och förstås rätt adress till w1_slave-filen som vanligt.

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import datetime
import syslog
import smtplib
from time import strftime
import sys
import os

syslog.openlog('aqua-temp')
syslog.syslog('Aqua-temp started')

# Definiera temperaturgranser
tempgoal = 25
alarmLow = tempgoal - 1
alarmHigh = tempgoal + 1
tempLow = tempgoal - 0.1
tempHigh = tempgoal + 0.1
lastKnownGoodTemp = tempgoal

# Klasser
class DS18B20Class:
	def __init__(self):
		self.power = 5 
	def init(self):
		syslog.syslog('DS18B20Class.init called.')
		GPIO.setup(self.power, GPIO.OUT)
	def off(self):
		syslog.syslog('DS18B20Class.off called.')
		GPIO.output(self.power, GPIO.LOW)
		time.sleep(30)
	def on(self):
		syslog.syslog('DS18B20Class.on called.')
		GPIO.output(self.power, GPIO.HIGH)
		time.sleep(30)
	def restart(self):
		syslog.syslog('DS18B20Class.restart called.')
		GPIO.output(self.power, GPIO.LOW)
		time.sleep(30)
		GPIO.output(self.power, GPIO.HIGH)
		time.sleep(30)

class heaterClass:
	def __init__(self):
		self.heater = 22 
	def init(self):
		syslog.syslog('heaterClass.init called.')
		GPIO.setup(self.heater, GPIO.OUT)
	def off(self):
		syslog.syslog('heaterClass.off called.')
		GPIO.output(self.heater, heatOff)
	def on(self):
		syslog.syslog('heaterClass.on called.')
		GPIO.output(self.heater, heatOn)

# Funktioner
def check_throttle():
	MESSAGES = {
		0: 'Under-voltage!',
		1: 'ARM frequency capped!',
		2: 'Currently throttled!',
		3: 'Soft temperature limit active',
		16: 'Under-voltage has occurred since last reboot.',
		17: 'Throttling has occurred since last reboot.',
		18: 'ARM frequency capped has occurred since last reboot.',
		19: 'Soft temperature limit has occurred since last rebot'
	}

	throttled_output = os.popen("vcgencmd get_throttled").readline()
	throttled_output = throttled_output.replace("throttled=","")
	throttled_output = throttled_output.strip()
	throttled_binary = bin(int(throttled_output, base=16))

	result = 'Throttle notices: ' 
	for position, message in MESSAGES.items():
		if len(throttled_binary) > position and throttled_binary[0 - position - 1] == '1':
        		result = result + ' ' + message
	
	return result

def measure_cpu_temp():
	temp = os.popen("vcgencmd measure_temp").readline()
	temp = temp.replace("temp=","")
	return temp.strip() + '. '

def sendMail(subject, msg):
	fromaddr = 'Me Self <me@self.com>'
	toaddr  = 'Me Self <me@self.com>'
	message = ('From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s' % (fromaddr, toaddr, subject, msg))
	username = 'username'
	password = 'password'

	server = smtplib.SMTP('smtp.gmail.com:587')
	server.starttls()
	server.login(username,password)
	try:
		server.sendmail(fromaddr, toaddr, message)
		syslog.syslog('Mail sent')
	except:
		syslog.syslog('Error sending mail')

	server.quit()

def writeTemp(temp):
	tfile = open('/var/www/html/temp.html', 'w')
	tfile.write('<html><head>')
	tfile.write('<title>Info för kar 1</title>')
	tfile.write('<META HTTP-EQUIV="refresh" CONTENT="60">')
	tfile.write('</head><body>')
	tfile.write(strftime("%Y-%m-%d %H:%M:%S"))
	tfile.write('<h1>Temperaturen i kar 1 är '+str(temp)+'°C</h1>')
	tfile.write('<h2>Värmaren är ')
	if heatStatus: tfile.write('på</h2>') 
	else: tfile.write('av</h2>')
	tfile.write('<p><a href="/mrtg/kar1.html">Historik</a>')
	tfile.write('</body></html>')
	tfile.close()
	
def checkTemp():
	global tempLarm
	global heatStatus
	global lastKnownGoodTemp
	sensorError = True

	fName = "/sys/bus/w1/devices/28-0415a841a3ff/w1_slave"
	temperature = lastKnownGoodTemp
	try:
		tfile = open(fName, 'r')
		text = tfile.read()
		tfile.close()
		if text:
			secondline = text.split("\n")[1]
			temperaturedata = secondline.split(" ")[9]
			temperature = float(temperaturedata[2:])
			temperature = float(int((temperature/100)+0.5))/10
			sensorError = False
	except:
		syslog.syslog('Error reading file: ' + str(sys.exc_info()[0]))

	if (temperature < 10 or temperature > 40):
		syslog.syslog('Error when reading temp: '+str(temperature))
		temperature = lastKnownGoodTemp
		sensorError = True
	else:
		lastKnownGoodTemp = temperature

	if (sensorError): DS18B20.restart()

	if (temperature < alarmLow or temperature > alarmHigh):
		if not tempLarm:
			syslog.syslog('Temp is wrong')
			sendMail("Temperaturen i kar 1 ar felaktig", "Temperaturen i kar 1 ar nu "+str(temperature))
			tempLarm = True
	else:
		if tempLarm:
			syslog.syslog('Temp is normal again')
			sendMail("Temperaturen i kar 1 ar normal", "Temperaturen i kar 1 ar nu "+str(temperature))
			tempLarm = False

	if (temperature <= tempLow):
		if not heatStatus:
			syslog.syslog('Temp is '+str(temperature)+'. Heater switched on')
			heater.on()
			heatStatus = True
	if (temperature >= tempHigh):
		if heatStatus:
			syslog.syslog('Temp is '+str(temperature)+'. Heater switched off')
			heater.off()
			heatStatus = False
	
	writeTemp(temperature)

# Variabler
DS18B20 = DS18B20Class()
heater = heaterClass()
heatOn = 0
heatOff = 1
tempLarm = False
heatStatus = False

# Initiera GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
heater.init()
heater.off()
DS18B20.init()
DS18B20.on()
syslog.syslog('GPIO initialized. Heater switched off, DS18B20 switched on')

# Huvudloop
while True:
	syslog.syslog('CPU temp: ' + measure_cpu_temp() + check_throttle())
	checkTemp()
	time.sleep(60)

För att skriptet ska starta automatiskt behöver man skapa filen aqua-temp.service i /lib/systemd/system med följande innehåll:

[Unit]
Description=Aqua-Temp
After=multi-user.target

[Service]
Type=idle
ExecStart=/usr/local/bin/aquacontrol/aqua-temp.py

[Install]
WantedBy=multi-user.target

Kör sedan

sudo chmod +x /usr/local/bin/aquacontrol/aqua-temp.py 
sudo chmod 644 /lib/systemd/system/aqua-temp.service 
sudo systemctl daemon-reload 
sudo systemctl enable aqua-temp.service

så ska det starta automatiskt vid omstart. Det är praktiskt att få loggen som skapas till en egen fil. Skapa filen /etc/rsyslog.d/aqua-temp.conf med innehållet

if $programname == 'aqua-temp' then /var/log/aqua-temp.log

Överkurs

Ibland kan det vara fiffigt att kunna hämta ut data i JSON-format. Jag har därför följande lilla fil, vid namn temp-xml.py, placerad i /var/www/cgi-bin. Ja, du behöver justera sökvägen till w1_slave…

#!/usr/bin/python

tfile = open("/sys/bus/w1/devices/28-011553897dff/w1_slave", 'r')
text = tfile.read()
tfile.close()

secondline = text.split("\n")[1]
temperaturedata = secondline.split(" ")[9]
temperature = float(temperaturedata[2:])
temperature = float(int((temperature/100)+0.5))/10

print ("Content-type: text/html\n")
print ('<?xml version="1.0" encoding="UTF-8"?>')
print ('<rss version="2.0">')
print ('  <item>')
print ('    <temp3>'+str(temperature)+'</temp3>')
print ('  </item>')
print ('</rss>')

För att Apache ska gå med på att köra python-script, och begripa vad cgi-bin är, behöver dessa rader läggas till i /etc/apache2/sites-available/000-default.conf efter DocumentRoot-raden

        ScriptAlias /cgi-bin/ /var/www/cgi-bin/
        <Directory "/var/www/cgi-bin">
                AllowOverride None
                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                Order allow,deny
                Allow from all
        </Directory>

Kör sedan

sudo chmod +x /var/www/cgi-bin/temp-xml.py 
sudo a2enmod cgi 
sudo systemctl restart apache2

så ska det hela rulla.

Share

Python 3

I Rasperrby Pi OS Buster installeras både Python 2 och Python 3 men Python 2 är default. Jag vill köra trean som standard vilket kräver lite pill.

sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 2

Verifiera att rätt version används med

python --version

Om det inte är trean som körs kan man tvinga vilken version man vill använda med

sudo update-alternatives --config python

Allt är inte förinstallerat för python3 så man får lägga till lite.

sudo apt install python3-rpi.gpio
Share

Nattlig omstart

Om man programmerar så dåligt som jag gör blir det lätt minnesläckor och annat som med tiden får pajen att stanna. Jag brukar lösa det genom att schemalägga en nattlig omstart. Kör

sudo crontab -e

och lägg in

0 4 * * * /sbin/shutdown -r now

sist så startar den om klockan fyra varje natt.

Share

LED-ramp ver #3

Jag har kört belysning baserat på adresserbara LED-strips ett tag och noterat både för- och nackdelar. Fördelarna är förstås styrningen av ljuset till både färg och styrka men nackdelarna är stora. De blir rätt snabbt gula i ljuset, i synnerhet om det blir lite för varma. 5 volt och många watt ger höga strömmar som bränner sönder de klena kablarna. Dags att prova något nytt! Nu blir det lite återgång till ursprunget då jag återigen bygger med reläer och lösa dioder men den här gången med sådana på 20W styck som drivs med 32V. 10 stycken till ett kar på 540 liter ska ge ca 23 lumen per liter (vilket är helt OK) och dra 200W vilket bara blir drygt 6 A totalt. 6 st vita (6500K), 2 st röda (660nm) och 2 st blå (445 nm) ska ge ett bra ljus för både växter och fiskar.

SD-kortet är preparerat med Raspberry Pi OS Lite med hjälp av Raspberry Pi Imager. Om man monterar SD-kortet igen på datorn efter att ha skrivit till det kommer man åt en partition som heter boot. Där kan man skapa en tom fil som heter ssh (ska fungera med ssh.txt också men det är oprövat) och en fil som heter wpa_supplicant.conf med följande innehåll:

country=SE
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
 
network={
ssid="your_real_wifi_ssid"
scan_ssid=1
psk="your_real_password"
key_mgmt=WPA-PSK
}

Du får förstås skriva in dina egna SSID och PSK-uppgifter men sedan är det bara att sätta SD-kortet i pajen, slå på strömmen och logga in via SSH. Inget behov av tangentbord och skärm med andra ord! Filerna som man skapade gör att SSH är aktiverat och att den automatiskt ansluter till WiFi.

Som vanligt börjar jag med att köra

sudo raspi-config

för att ställa in diverse parametrar. Eftersom jag planerar att köra min vanliga temperaturstyrning slår jag bland annat på 1-wire under Interfacing options. Filsystemet utökas som vanligt under Advanced options och sedan är det dags för en omstart innan jag uppdaterar systemet med

sudo apt update 
sudo apt upgrade

Sedan är det bara resten kvar!

Share