Montag, 5. Dezember 2016
Hauststeuerung - Heizungssteuerung
Und schon sind wir bei einem der brandaktuellsten Themen angekommen. Herz unser Heizung ist ein Pellet-Kessel von Viessmann, ein Vitolig 300 VL3A. Im EG + UG haben wir damals ganz klassisch Raumtemperaturregler (Busch Jäger 1095U) verbaut, die per Bimetall und Relais die Stellmotoren im Heizkreisregler steuern. Zusätzlich gibt es eine Fernbedienung, über die alle Soll-Temperaturen und Heizzeiten eingestellt werden können. Funktionierte soweit auch.
Im OG habe ich auf die Raumtemperaturregler verzichtet, da ich ja überall meine 1 Wire Temperatursensoren verteilt habe. Als Stellantriebe im Verteiler setze ich Alpha 5 mit einer Betriebsleistung von 1 Watt ein. Gesteuert werden die Antriebe elektrisch über ein HomeMatic 8Kanal Empfangsmodul HM-MOD-Re-8 eQ-3, welches per 8fach Relais dann die Motoren schaltet. Anders als bei herkömmlichen Thermostaten gibt es nur 2 Zustände - an und aus - was eine Steuerlogik sehr vereinfacht.
In Fhem habe ich jetzt pro Raum ein virtuelles Thermostat angelegt, welches anhand der Ist Temperatur, der Soll Temperatur und der eingestellten Hysterese (vereinfacht gesagt ist das einfach ein Toleranzbereich, der Über-/Unterschritten werden kann, bevor geschaltet wird), die Relais ein- und ausschaltet.

Benötigt wird dazu ein Dummy für die Soll Temperatur des Raumes:

define OG.SZ.TempDes dummy
attr OG.SZ.TempDes group 4: Heizung
attr OG.SZ.TempDes room OG.Schlafzimmer
attr OG.SZ.TempDes setList state:20,21,22,23,24
attr OG.SZ.TempDes webCmd state

Das Reading der Ist Temperatur und das Modul Threshold:

define OG.SZ.TH THRESHOLD OG.SZ.Temp:temperature:0.2:OG.SZ.TempDes:state HM_35F8F0_Sw_03
attr OG.SZ.TH group 4: Heizung
attr OG.SZ.TH number_format %.1f
attr OG.SZ.TH room OG.Schlafzimmer
attr OG.SZ.TH setOnDeactivated cmd1_gt
attr OG.SZ.TH state_cmd1_gt off
attr OG.SZ.TH state_cmd2_lt on
attr OG.SZ.TH state_format _m _dv _sc

So, dass funktioniert also schön. Temperatur nicht erreicht - Stellmotor auf, Temperatur erreicht - Stellmotor zu. Ganz einfach oder :)

Die Probleme fingen an, als unser Monteur den zweiten Heizkreis am Kessel in Betrieb genommen hat. Mal funktionierte die Pumpe, mal nicht. Nachdem das Steuermodul getauscht wurde und ich beim Aufheizen des Estrichs quasi nur im Handbetrieb halbwegs vernünftige Temperaturen erreichen konnte, hatte ich die Nase voll. Selbst der Viessmann Kundendienst konnte nicht mehr weiter helfen und wolle anfangen, auf Verdacht Steuerplatinen und Netzteile zu tauschen - natürlich auf unsere Kosten. Aber nicht mit mir 1000€ nur für Experimente auf meinem Rücken hab ich nicht eingesehen. Also was blieb mir anderes übrig, als mich noch weiter mit der Heizung auseinanderzusetzen. Größte Herausforderung ist der Heizkreismischer, der dafür sorgt, dass entsprechend der Außentemperatur und der eingestellten Heizkurve die richtige Vorlauftemperatur eingestellt wird. Zum Glück gibt es auch andere Anwender, die sich dazu schon Gedanken gemacht haben und u.a. dafür das Modul Stellmotor entwickelt haben. Softwareseitig also halbwegs gelöst. Die Hardware ist aber auch nicht wirklich dramatisch (Bitte ggf. aber einen Fachmann ran lassen, da 230V!).
Was müssen wir tun:
- Mischermotor steuern (bei mir ähnlich einem Rolladenmotor, gibt es eine Leitung für den Rechts- und eine für den Linkslauf)
- Umwälzpumpe bedarfsgerecht ein- und ausschalten

Für das physische Schalten verwende ich die GPIOs des Raspberries und schalte damit ein Relais. Ich setze hier keine zusätzlichen Aktoren ein, da ich keine Delays haben möchte und keine Komponenten, die ggf. nicht funktionieren. Achtet bei den Relais darauf, dass diese bereits eine Sperrdiode verfügen, um den durch die Spule induzierten Strom abzufangen, sonst müsst Ihr das in Eurer Schaltung berücksichtigen. Da das Relais mit 3,3V nicht zuverlässig schaltet, verwende ich 5V und schalte das Relais per Transistor. Den Transitor deshalb, da Ihr Euch sonst die GPIOs des Raspberry zerschießt.
Folgende Bauteile werden benötigt:
- Widerstand 10kΩ
- Widerstand 2KΩ
- NPN Transistor z.B. BC548

Und so habe ich alles miteinander verdrahtet:
->>>>>>>>> Bild einfügen

Zusätzlich zu den zu schaltenden Relais brauchen wir auch wieder ein paar 1 Wire Temperatursensoren. Für die softwareseitige Umsetzung werfen wir jetzt mal alles zusammen:

define Mischer.OG.zu RPI_GPIO 18
attr Mischer.OG.zu direction output
attr Mischer.OG.zu eventMap on:1 off:0
attr Mischer.OG.zu group GPIO
attr Mischer.OG.zu poll_interval 1
attr Mischer.OG.zu room Heizung,System

define Mischer.OG.auf RPI_GPIO 17
attr Mischer.OG.auf direction output
attr Mischer.OG.auf eventMap on:1 off:0
attr Mischer.OG.auf group GPIO
attr Mischer.OG.auf poll_interval 1
attr Mischer.OG.auf room Heizung,System

-> damit ist jetzt die GPIO Steuerung für den Mischer definiert

define Heizung.OG.Pumpe RPI_GPIO 2
attr Heizung.OG.Pumpe alias Heizung.OG.Pumpe
attr Heizung.OG.Pumpe comment Pin3
attr Heizung.OG.Pumpe direction output
attr Heizung.OG.Pumpe group GPIO
attr Heizung.OG.Pumpe poll_interval 1
attr Heizung.OG.Pumpe room Heizung,System

-> damit können wir das Relais für die Pumpe ansteuern

define Heizung.OG.Mischer STELLMOTOR FhemDev
attr Heizung.OG.Mischer STMcalibrateDirection L
attr Heizung.OG.Mischer STMdebugToLog3 0
attr Heizung.OG.Mischer STMfhemDevRL Mischer.OG.auf
attr Heizung.OG.Mischer STMfhemDevSTART Mischer.OG.zu
attr Heizung.OG.Mischer STMinvertOut 0
attr Heizung.OG.Mischer STMlastDiffMax 1
attr Heizung.OG.Mischer STMmapOffCmd 0
attr Heizung.OG.Mischer STMmapOnCmd 0
attr Heizung.OG.Mischer STMmaxDriveSeconds 118
attr Heizung.OG.Mischer STMmaxTics 100
attr Heizung.OG.Mischer STMpollInterval 0.1
attr Heizung.OG.Mischer STMresetOtherDeviceAtCalibrate 0
attr Heizung.OG.Mischer STMrlType einzel
attr Heizung.OG.Mischer STMtimeTolerance 0.01
attr Heizung.OG.Mischer group Steuerung
attr Heizung.OG.Mischer room Heizung

-> Steuerung Mischermotor

define Heizung.OG.MischerCommands DOIF ([Heizung.OG.VorlaufIst:temperature.avg] < 10)\
(set Heizung.OG.Mischer 1)\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] > 49)\
(set Heizung.OG.Mischer 1)\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] < ([Heizung.OG.VorlaufSollClone]-10) and [Heizung.OG.Mischer:position]<97 and [Heizung.OG.Pumpe] eq "on")\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]+3)}) ## Cmd 3: Schnell oeffnen\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] < ([Heizung.OG.VorlaufSollClone]-6) and [Heizung.OG.Mischer:position]<98 and [Heizung.OG.Pumpe] eq "on")\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]+2)}) ## Cmd 4: Ganz langsam oeffnen\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] < ([Heizung.OG.VorlaufSollClone]-2) and [Heizung.OG.Mischer:position]<98 and [Heizung.OG.Pumpe] eq "on")\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]+1)}) ## Cmd 5: Noch ein bisschen\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] > ([Heizung.OG.VorlaufSollClone]+5) and [Heizung.OG.Mischer:position]>3)\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]-2)}) ## Cmd 6: Viel zu heiss, schnell zu\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] > ([Heizung.OG.VorlaufSollClone]+3) and [Heizung.OG.Mischer:position]>2)\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]-1)}) ## Cmd 7: Zu heiss, schnell zu\
DOELSEIF ([Heizung.OG.VorlaufIst:temperature.avg] > ([Heizung.OG.VorlaufSollClone]+1) and [Heizung.OG.Mischer:position]>2)\
(set Heizung.OG.Mischer {([Heizung.OG.Mischer:position]-1)}) ## Cmd 8: Noch ein bisschen\
DOELSEIF ([03:50])\
(set Heizung.OG.Mischer calibrate) ## Cmd 9: Calibrieren fuer Tagprogramm\
DOELSE()
attr Heizung.OG.MischerCommands cmdpause 0:0:30:60:90:60:120:180:0:0
attr Heizung.OG.MischerCommands do always
attr Heizung.OG.MischerCommands group Steuerung
attr Heizung.OG.MischerCommands room Heizung
attr Heizung.OG.MischerCommands wait 0,0,90,120,150,180,240,480,0,0

-> Logik für das Mischen der Temperatur, die ersten 2 Kommandos schließen den Mischer zur Sicherheit, wenn aus irgendeinem Grund keine Vorlauftemperatur gelesen wird bzw. der Vorlauf wegen der Schwerkraft (Wärme zirkuliert auch wenn die Pume aus ist) oder wegen der Pufferladepumpe zu heiß ist

define FHEM FHEM2FHEM 192.168.1.39:7072 LOG:Heizung.*
attr FHEM room System

-> Verbindung zur Hauptinstanz herstellen

define Heizung.OG.VorlaufSollClone cloneDummy Heizung.OG.VorlaufSoll
attr Heizung.OG.VorlaufSollClone group Temperaturen
attr Heizung.OG.VorlaufSollClone room Heizung
attr Heizung.OG.VorlaufSollClone stateFormat {sprintf(ReadingsVal("Heizung.OG.VorlaufSollClone","temperature",30))}

define Heizung.OG.PumpenanforderungClone cloneDummy Heizung.OG.Pumpenanforderung
attr Heizung.OG.PumpenanforderungClone group Steuerung
attr Heizung.OG.PumpenanforderungClone room Heizung
attr Heizung.OG.PumpenanforderungClone stateFormat {sprintf(ReadingsVal("Heizung.OG.PumpenanforderungClone","status","off"))}

-> von der Haussteuerung bekomme ich jetzt das Soll der Vorlauftemperatur und die Info, ob die Pumpe laufen soll

define Sensorencheck DOIF ([Heizung.OG.VorlaufIst:temperature] == 0) (set Pushover msg 'FHEM' 'Heizungssensoren ausgefallen - Neustart notwendig!')
attr Sensorencheck group Benachrichtigungen
attr Sensorencheck room System

-> da es alle paar Wochen mal passiert, dass der 1 Wire ausfällt, lass ich mich für den Fall per Push Nachricht auf´s Handy informieren, ich hab keine Lust, noch einen Busmaster für 1 Wire zu kaufen, damit würde das nicht mehr passieren. Aber so lange das nur 1x alle 5-6 Wochen passiert, sitz ich das aus ;)

define Heizung.OG.VorlaufIst GPIO4 28-80000026e279
attr Heizung.OG.VorlaufIst group Temperaturen
attr Heizung.OG.VorlaufIst model DS18B20
attr Heizung.OG.VorlaufIst room Heizung
attr Heizung.OG.VorlaufIst stateFormat {sprintf (ReadingsVal("Heizung.OG.VorlaufIst","temperature",0))}
attr Heizung.OG.VorlaufIst userReadings temperature.avg {movingAverage("Heizung.OG.VorlaufIst","temperature",300)}

-> die Ist Temperatur des Vorlaufs, ich verwende hier den Gleitenden Mittelwert, um ein ständiges Gegensteuern des Mischers etwas abzumildern.

sub movingAverage($$$){
my ($name,$reading,$avtime) = @_;
my $hash = $defs{$name};
my @new = my ($val,$time) = ($hash->{READINGS}{$reading}{VAL},$hash->{READINGS}{$reading}{TIME});
my ($cyear, $cmonth, $cday, $chour, $cmin, $csec) = $time =~ /(\d+)-(\d+)-(\d+)\s(\d+):(\d+):(\d+)/;
my $ctime = $csec+60*$cmin+3600*$chour;
my $num;
my $arr;
#-- initialize if requested
if( ($avtime eq "-1") ){
$hash->{READINGS}{$reading}{"history"}=undef;
}
#-- test for existence
if( !$hash->{READINGS}{$reading}{"history"}){
#Log 1,"ARRAY CREATED";
push(@{$hash->{READINGS}{$reading}{"history"}},\@new);
$num = 1;
$arr=\@{$hash->{READINGS}{$reading}{"history"}};
} else {
$num = int(@{$hash->{READINGS}{$reading}{"history"}});
$arr=\@{$hash->{READINGS}{$reading}{"history"}};
my $starttime = $arr->[0][1];
my ($syear, $smonth, $sday, $shour, $smin, $ssec) = $starttime =~ /(\d+)-(\d+)-(\d+)\s(\d+):(\d+):(\d+)/;
my $stime = $ssec+60*$smin+3600*$shour;
#-- correct for daybreak
$stime-=86400
if( $stime > $ctime);
if( ($num < 25)&&( ($ctime-$stime)<$avtime) ){
#Log 1,"ARRAY has $num elements, adding another one";
push(@{$hash->{READINGS}{$reading}{"history"}},\@new);
}else{
shift(@{$hash->{READINGS}{$reading}{"history"}});
push(@{$hash->{READINGS}{$reading}{"history"}},\@new);
}
}
#-- output and average
my $average = 0;
for(my $i=0;$i<$num;$i++){
$average+=$arr->[$i][0];
Log 4,"[$name moving average] Value = ".$arr->[$i][0]." Time = ".$arr->[$i][1];
}
$average=sprintf( "%5.3f", $average/$num);
#--average
Log 4,"[$name moving average] calculated over $num values is $average";
return $average;
}

-> Den Code müsst Ihr in Eure "99_myUtils.pm" kopieren. Zu finden bei "edit files". Falls das File noch nicht existiert, öffnet Ihr die "myUtilsTemplate.pm" und speichert sie als "99_myUtils.pm" ab.

Jetzt noch das notwendige Setup auf dem Haupt-FHEM:

define Heizung.OG.Neigung dummy
attr Heizung.OG.Neigung group 4: Heizung
attr Heizung.OG.Neigung room UG.Heizungskeller
attr Heizung.OG.Neigung setList state:0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0
attr Heizung.OG.Neigung webCmd state

define Heizung.OG.Niveau dummy
attr Heizung.OG.Niveau group 4: Heizung
attr Heizung.OG.Niveau room UG.Heizungskeller
attr Heizung.OG.Niveau setList state:0,1,2,3,4,5,6
attr Heizung.OG.Niveau webCmd state

define Heizung.OG.Verstaerkung dummy
attr Heizung.OG.Verstaerkung group 4: Heizung
attr Heizung.OG.Verstaerkung room UG.Heizungskeller
attr Heizung.OG.Verstaerkung setList state:0.0,0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0
attr Heizung.OG.Verstaerkung webCmd state

define Heizung.OG.VorlaufMin dummy
attr Heizung.OG.VorlaufMin group 4: Heizung
attr Heizung.OG.VorlaufMin room UG.Heizungskeller
attr Heizung.OG.VorlaufMin setList state:25,26,27,28,29,30
attr Heizung.OG.VorlaufMin webCmd state

define Heizung.OG.VorlaufMax dummy
attr Heizung.OG.VorlaufMax group 4: Heizung
attr Heizung.OG.VorlaufMax room UG.Heizungskeller
attr Heizung.OG.VorlaufMax setList state:45,46,47,48,49,50
attr Heizung.OG.VorlaufMax webCmd state

define Heizung.OG.VorlaufSoll dummy
attr Heizung.OG.VorlaufSoll group 4: Heizung
attr Heizung.OG.VorlaufSoll room UG.Heizungskeller
attr Heizung.OG.VorlaufSoll userReadings temperature { ReadingsVal ("Heizung.OG.VorlaufSoll", "state",30);;;;}

define Heizung.OG.VorlaufSollRefresh at +*00:10:00 { prg_Vorlauf_OG() }
attr Heizung.OG.VorlaufSollRefresh group 4: Heizung
attr Heizung.OG.VorlaufSollRefresh room UG.Heizungskeller

-> Das sind die Paramter, die ich auch am Kessel einstellen könnte. Ggf. müsst Ihr mit den Parametern etwas rumprobieren.

sub
prg_Vorlauf_OG()
{
my $steil = ReadingsVal("Heizung.OG.Neigung","state","0.4");
my $kh = ReadingsVal("Heizung.OG.Niveau","state","1");
my $verst = ReadingsVal("Heizung.OG.Verstaerkung","state","1");
my $minvl = ReadingsVal("Heizung.OG.VorlaufMin","state","25");
my $maxvl = ReadingsVal("Heizung.OG.VorlaufMax","state","50");
my $sw = ReadingsVal("OG.KZ.TempDes","state","22");
my $iw = ReadingsVal("OG.KZ.Temp","temperature","20");
my $at = ReadingsVal("E_WS1","temperature","10");

my $t1 = ($at/(320-$at*4));
my $t2 = pow($sw,$t1);
my $maxabw = $sw-$iw;

my $swhk = (((0.55)*$steil*($t2)*((-$at+20)*2)+$sw+$kh)+($maxabw*$verst));
if ($swhk < $minvl) {$swhk=$minvl};
if ($swhk > $maxvl) {$swhk=$maxvl};
$swhk = int(100*$swhk+0.5)/100;
fhem ("set Heizung.OG.VorlaufSoll $swhk");
}

-> Das ist der Code zur Berechnung der Innen- und Außentemperaturabhängigen Vorlauf Soll Temperatur.

define OG.Heizung.Sommerbetrieb dummy
attr OG.Heizung.Sommerbetrieb devStateIcon on:weather_sun off:weather_frost
attr OG.Heizung.Sommerbetrieb group 4: Heizung
attr OG.Heizung.Sommerbetrieb room UG.Heizungskeller
attr OG.Heizung.Sommerbetrieb setList on off

define Heizung.OG.Pumpenanforderung dummy
attr Heizung.OG.Pumpenanforderung group 4: Heizung
attr Heizung.OG.Pumpenanforderung room UG.Heizungskeller
attr Heizung.OG.Pumpenanforderung userReadings status { ReadingsVal ("Heizung.OG.Pumpenanforderung", "state","off");;;;}

define Heizung.OG.PumpenanforderungCommands DOIF ([+0:01] and [OG.Heizung.Sommerbetrieb] eq "off" and \
([?HM_35F8F0_Sw_06] eq "on" or \
[?HM_35F8F0_Sw_03] eq "on" or\
[?HM_35F8F0_Sw_02] eq "on"))\
(set Heizung.OG.Pumpenanforderung on) ## 6 KZ, 3 SZ, 2 Bad\
DOELSE\
(set Heizung.OG.Pumpenanforderung off)
attr Heizung.OG.PumpenanforderungCommands group 4: Heizung
attr Heizung.OG.PumpenanforderungCommands room UG.Heizungskeller

-> Mit dem dummy Sommerbetrieb, kann ich sowohl die Ansteuerung der Stellmotoren deaktivieren, als auch die Umwälzpumpe (indirekt, da die Pumpenanforderung in dem Fall nie auf "on" schaltet).

Da das Setup im Obergeschoss sehr stabil lief, habe ich kurzerhand dem Kessel auch die Kontrolle über den Heizkreis im EG/UG entzogen. Mangels 1 Wire im "Altbau" musste ich auf einen STM 330 Temperatursensor zurückgreifen. Bisher habe ich erstmal nur einen im Wohnzimmer (ca. 50€ pro Stück ist schon ziemlich heftig), aber auch damit funktioniert das Setup hervorragend. Letztendlich richtet sich somit alles nach dem Wohnzimmer. Da ich im EG/UG aber immer noch die ganz am Anfang genannten analogen Raumtemperaturregler verwende, hab ich die in den anderen Räumen etwas höher gedreht und im Wohnzimmer etwas runter. Läuft...
Die Steuerung im Kessel ist jetzt nur noch dafür verantwortlich, dass der Puffer geladen wird und Warmwasser bereit steht - sollte er ja wohl noch hinbekommen ;)

Irgendwann werd ich in den restlichen Zimmern auch noch Temperatursensoren nachrüsten. Sobald das erledigt ist, kann ich auch die Stellmotoren im EG/UG in die Haussteuerung einbinden.