Używamy plików cookies (tzw. ciasteczka) by spersonalizować treść i ogłoszenia oraz by analizować ruch na stronie.  W sposób automatyczny dzielimy się informacjami o Twoim użyciu tego portalu z dostawcami ogłoszeń, którzy mogą połączyć te informacje z informacjami, które im udzieliłaś/łeś lub, które sami zebrali. Korzystanie z witryny bez zmiany ustawień dotyczących cookies oznacza, że będą one zamieszczane w Państwa urządzeniu końcowym.  Możecie Państwo dokonać w każdym czasie zmiany ustawień dotyczących cookies zmieniając opcje przeglądarki.

Komunikacja z Viessmann'em Vitodens 200

Piec jest urządzeniem, które zużywa największą ilość energii, i którego obsługa kosztuje najwięcej. Nic więc dziwnego, że od samego początku nawiązanie połączenia z moim Viessmann’em było nieustającym przedmiotem moich zainteresowań.

Wszystkim, którzy posiadają piece Viessmann’a proponuję uważną lekturę strony openv.wikispaces.com. Autorzy wykonali olbrzymią pracę, by rozszyfrować wewnętrzny protokół pieców. Opublikowali też schematy urządzeń służących do nawiązania połączenia. Chwała im!

Gdy otworzymy klapkę Vitodensa 200 (a wierzę, że podobnie jest też z innymi modelami), oczom naszym ukaże się włącznik i dwie lampki po obu stronach litery V. Lewa podpisana jest symbolem błyskawicy i sygnalizuje błędy/awarie. Druga świeci się na zielono, gdy piec jest włączony. To jednak nie wszystko. Jedna z tych lampek jest nadajnikiem IR, druga odbiornikiem. Wgłębienie w kształcie litery V umożliwia zamontowanie kabla serwisowego.

Panowie z OpenV(.wikispaces.com) opracowali kilka urządzeń, które zastąpić mogą firmowy kabel Viessmanna, kosztujący w chwili pisania tego artykułu ok 130 EUR. 130 EUR to bardzo dużo, rzec by można, że to absurdalna kupa kasy za coś, co można łatwo wykonać samodzielnie.

Na pierwszy ogień poszedł adapter USB wg schematu z OpenV. Zamieszczam tu plik pdf z ich strony ze schematem, objaśnieniami i obrazem płytki.  Mam nadzieję, żę nie będą mieli nic przeciwko.

Oryginalny projekt nieco przebudowałem, by płytka mieściła się w najmniejszej obudowie Kradex’u, kupiłem wszystkie elementy (diodę IR i fototranzystor znalazłem tylko na farnell.eu, przesyłka z Anglii szła 24h) i zabrałem się do roboty.

Wykonanie płytki metodą termotransferu w warunkach domowych przy tak niewielkich elementach, jakie są potrzebne pod chip FT232RL, jest trudne. Albo temperatura za niska i toner się nie odkleja, albo za wysoka i się rozmazuje… Potrzebowałem kilku prób i pogodzenia się z tym, że nie wszystko będzie idealnie i pięknie. Oto obraz płytki i gotowy ‘adapter’:

USB Adapter USB1 USB2 USB3

Wszystko tu jest trochę pokrzywione i niedoskonałe, ale tak to jest z pierwszym modelem. Na jego bazie skorygowałem płytkę i niektóre założenia. Gdyby ktokolwiek był zainteresowany samodzielny wykonaniem, oto link do obrazu płytki.

Gotowy adapter przykleiłem do pieca taśmą dwustronnie klejącą, wetknąłem wtyczkę do mojego laptopa, uruchomiłem program v-control (w wersji 3 beta) i… wszystko zadziałało. Radość niezmierna, oto przed sobą miałem wszystkie parametry mojego pieca!

Docelowo jednak za komunikację odpowiedzialny miał być Raspberry Pi, który jest w szafie sterowniczej odległej o jakieś 4m kabla. Gdy podłączyłem ‘adapter’ do skrętki czekającej w okolicach pieca, a z drugiej jest strony, w szafie, wpiąłem kabel z wtyczką USB, mój laptop zameldował, iż nie rozpoznaje urządzenia… odległość za duża, kabel nie ten, generalnie… projekt upadł.

Przy drugim podejściu postanowiłem uprościć sobie życie. Kupiłem na aliexpress adapter USB-RS232 na bazie FT232RL za 5 USD z przesyłką. Przedzieliłem schemat płytki na pół i zrobiłem wyłącznie część „po” FT232RL:

USB-FT232RL Small RS232-V RS232-V 2 Small1


Płytka jest malutka (28x14mm), płaska na tyle, że możliwe jest zamknięcie klapki pieca. Przymocowana jest dwustronną taśmą z pianki w której dziurkaczem wyciąłem 2 otwory. Dzięki podzieleniu modułu na 2 części, konwersja USB na RS232 odbywa się w szafie rozdzielczej, przy Raspberry Pi. RS232 okazuje się odporny na zakłócenia i doskonale radzi sobie z 4m skrętki. Wszystko działa! Gdyby ktoś był zainteresowany obrazem płytki, oto plik pdf. Rezystory są rozmiaru 1206.  Diodzie i fototranzystorowi trzeba nieco uciąć nózki.

Raspberry działa pod linuxem, więc – niestety – trzeba być gotowym na trochę pracy przez terminal. Udostępniany na openv.wikispaces.com daemon vcontrold działał, jednakże tylko przy wykorzystaniu protokołu KW, który jest stosunkowo wolny (odczyt 20 adresów to ok 1,5 minuty). Przy komunikacji protokołem 300 działał tylko odczyt. Po wielu próbach zrozumienia, o co chodzi i czemu….poddałem się i zainstalowałem ViTalk (również z openv.wikispaces.com). Do vcontrold jeszcze wrócę, czekam na odpowiedź na forum...

ViTalk jet prosty i mały. Działa jak marzenie. Ostatecznie rozbudowałem go trochę (źródło jest w C, trzeba trochę wysilić głowę) o funkcję pozwalającą zczytywanie dowolnego adresu. (przykładowe użycie: "raw 0x2323"). Generalnie brak jest przejrzystej dokumentacji co, który adres dla której wersji pieca znaczy. Trzeba próbować, próbować i próbować.

Gdyby ktoś był zainteresowany roboczą, nieco przerobioną wersją ViTalk’a, niech da znać, podeślę pliki źródlowe. 

W całym tym linuxowym środowisku czuję się bardzo niepewnie. Nie będę więc opisywał poszczególnych kroków. Proponuję lekturę instrukcji przygotowanej na http://openv.wikispaces.com/vcontrold+mit+Raspberry+Pi.

Vitalk (jak ja to rozumiem) jest serwerem telnetowym, który pośredniczy w komunikacji z Viessmann’em. Można do niego połączyć się bezpośrednio z konsoli (telnet localhost 83) i wpisywać komendy (np. „g power” by otrzymać aktualną moc palnika, lub „raw 0x2000 2” by odczytać 2 znaki z adresu 2000). Można połączyć się przez skrypt w PYTHONIE, który będzie dane wysyłał do bazy SQL, z której potem można czerpać dane do wykonania wykresów. Można napisać skrypt PHP, który udostępni bieżące dane przez stronę www i umożliwi sterowanie przez przeglądarke (lub komunikację z WAGO). Można też (chyba) połączyć się z nim bezpośrednio ze sterownikiem WAGO (z pominięciem serwera www).

Oto kilka plików, jako pomoc i inspiracja do własnych poszukiwań:

1. Skrypt w PYTHONie (vito.py) wysyłający dane do bazy SQL, w której jest tabela Vito z kolumnami „time”, „comm”, „value”. Rozbudowałem go nieco, by wysyłał tylko dane, które się zmieniły, a co godzinę przesyłał cały zestaw. Aby plik wykonywał się samoczynnie co 1 min należy w cronie (crontab –e) dodać linię:

*/1 * * * * /var/python/vito.py > /dev/null 2>&1

#!/usr/bin/python
import telnetlib
import MySQLdb
	
from time import  localtime, strftime, strptime, mktime
from datetime import timedelta, datetime
	
timer = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
timer2 = (datetime.now()-timedelta(seconds=1)).strftime("%Y-%m-%d %H:%M:%S")
	
commands = ["mode","power", "k_soll_temp", "k_ist_temp", "ww_soll_temp", "ww_ist_temp", "outdoor_temp"]
	
try:
	tn = telnetlib.Telnet("localhost", 83)
except:
	print "Connection ERROR"
else:
	tn.read_until(b"$", 5)
	replies=[]
	
	# try to read file with previous values
	try:
		storageFile = open('storedData.txt','r')
	except:
		previousData = False
	else:
		previousData = storageFile.read()
		previousDataA = previousData.split("\n")
		i=0
		for line in previousDataA:
			previousDataA[i] = line.split(";")
			i+=1	
		storageFile.close()
	
	# prepare file for writting
	try:
		storageFile = open('storedData.txt', 'w')
	except:
		storageFile = False
	
	# execute commands via telnet
	i=0
	print "Checking Vito data: "
	for command in commands:
		tn.write(b"g "+command+"\n")
		reply=tn.read_until(b"$", 5).strip("\n$")
		print(commands[i].rjust(20)+": "+reply+", previous: "+(previousDataA[i][0] if (previousData!=False and i<len(previousDataA)) else "missing"))
		if storageFile!=False:
			storageFile.write(reply+";")
		if previousData!=False and i<len(previousDataA) and previousDataA[i][0]!= reply:
			storageFile.write("1;\n")
		else :
			storageFile.write("0;\n")
		replies.append(reply)
		i+=1
	print ""
	
	tn.close()
	
	#check for the last timestamp and decide to send full record set
	if previousData!=False :
		try:
			lastTime = strptime(previousDataA[-1][0].strip(),"%Y-%m-%d %H:%M:%S")
		except:
			print "No previous timestamp found"
			enforceFrame=False
			storageFile.write(timer)
		else:
			lastTime = datetime.fromtimestamp(mktime(lastTime))
			timeSinceFrame = datetime.now() - lastTime
			print "Time since last frame: "+str(timeSinceFrame)+ " (H:M:S.ms)"
			if timeSinceFrame.seconds > 3600 :
				enforceFrame = True
				storageFile.write(timer)
				print "Sending full frame"
			else:
				enforceFrame = False
				storageFile.write(previousDataA[-1][0]+";")
	print ""
	
	storageFile.close()
	
	# sending data to SQL
	print "Sending data to database:"
	try:
    		db = MySQLdb.connect("SERVER","USER","PASSWORD","DATABASE" )
  	except:
    		print timer + " : Error connecting to the SQL database"
	else:
    		cursor = db.cursor()
		i=0
		for reply in replies:
			print commands[i].rjust(20)+" : ",
			if enforceFrame or previousData == False  or (previousData!=False and i<len(previousDataA) and reply!=previousDataA[i][0]):
    				try:
      					cursor.execute("INSERT INTO Vito (time, comm, value) VALUES (%s, %s, %s)", (timer, commands[i], float(reply)*10))
      					db.commit()
      					print  "Sending data ok, ",
					if previousDataA[i][1]=="0":
						cursor.execute("INSERT INTO Vito (time, comm, value) VALUES (%s, %s, %s)", (timer2, commands[i], float(previousDataA[i][0])*10))
						db.commit()
	      					print  "including pre-data"
					else :
						print "single data"
    				except:
      					db.rollback()
      					print "Error executing query"
			else:
				print "No new data.  Nothing sent"
			i+=1
    		db.close() 

2. Plik PHP (GetData1.php) odczytujący dane z bazy SQL i wysyłający je w formacie JSON. W zmiennej $commands wymienione są wszystkie komendy (wartości pola ‘comm’), które mają być zwrócone a w $desc opisy do wyświetlenia w legendzie wykresów. W nawiasach na końcu ( ) podawane są jednostki.

<?php
	$commands = array("mode", "power", "raum_soll_temp", "raum_ist_temp", "k_soll_temp", "k_ist_temp", "ww_soll_temp", "ww_ist_temp", "outdoor_temp");
	$desc = array("program(0)", "moc palnika(%)", "temp. pokoju-cel(t)", "temp. pokoju(t)", "temp. kotła-cel(t)", "temp. kotła(t)", "temp. c.wody cel(t)", "temp. c. wody(t)", "temp. zewn.(t)");
	
	
	$link  = mysqli_connect("SERVER", "USER", "PASSWORD","DATABASE");
	if (!$link) {
		printf("Connect failed: %s\n", mysqli_connect_error());
		exit();
	}
	IF  (isset($_POST['DATEFROM']) and $_POST['DATEFROM']!=='') { 	
		$datefrom=$link->real_escape_string($_POST['DATEFROM']);
	}	
	ELSE {
		$phpdate=time()- (1*6 * 60 * 60);
		$datefrom = date( 'Y-m-d H:i:s', $phpdate );
	};
	IF  (isset($_POST['DATETO']) and $_POST['DATETO']!=='')	{
		$dateto=$link->real_escape_string($_POST['DATETO']);
	}	
	ELSE {
		$dateto = date( 'Y-m-d H:i:s', time() );
	};
	
	echo '{"series": [';
	
	$commandsCount = count($commands);
	
	for ($i=0; $i<$commandsCount; $i++) {	
		
		$query = "SELECT * FROM Vito WHERE `time` > '$datefrom' AND `time` < '$dateto' AND `comm` = '$commands[$i]' ORDER BY time ASC";
		
		if ($result = mysqli_query($link, $query)) {
			$rowcount = mysqli_num_rows($result);
			$counter=1;
			echo '{"name": "'.$desc[$i].'", "data":[';
					
			while($r = mysqli_fetch_assoc($result)) {
				echo '['.strtotime($r["time"])*1000 . ', '.$r["value"]/10 .']';
				
				if($counter < $rowcount){ 
					echo ", "; 
				}
				$counter=$counter+1;
			}
			echo'  ], "unit": 0}';
			
			if ($i < ($commandsCount-1)) {
				echo ',';
			}
			mysqli_free_result($result);
			
		}
	};	
	
	echo ']}';
	mysqli_close($link);
?>

3. Plik HTML do umieszczenia na serwerze www, którzy przygotowuje wykresy. Wykorzystuje biblioteki highcharts oraz jQuery i jQueryUI.

<!DOCTYPE HTML>
<html>
	<head>
		<html xmlns="http://www.w3c.org/1999/xhtml" xml:lang="pl" lang="pl">	
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<title>Wykresy Vitodens 200</title>
		
		
		<script type="text/javascript" src="/js/jquery.min.js"></script>	
		<script type="text/javascript" src="/js/jquery-ui.custom.min.js"></script>	
		<link type="text/css" href="/css/ui-lightness/jquery-ui.custom.min.css" rel="stylesheet" />	
		
		
		<script type="text/javascript" src="/HighChart/js/highcharts.js"></script>
		<script type="text/javascript" src="/HighChart/js/themes/grid.js"></script>		
		
		<script type="text/javascript">
	 		
			var chart;
			var options; 
			
			function UpdateChart() {
				$.ajax({
					url: 'http://YOUR_SERVER/GetData1.php',
					type: 'POST',
					dataType: 'json',
					data: {DATEFROM: $('#DateFrom').val(), DATETO: $('#DateTo').val()},
					success: function(data) {
						var options1 = $.extend({},data, options, {title:{text: ""}});		
						$('#container').highcharts(options1);
					}
				});	
			};
			var programs = {
				0: "OFF",
				1: "Tylko c. woda",
				2: "Woda i grzanie"
			};
			var tooltipFormatter = {
				"t": function (point) {
					return Highcharts.dateFormat('%a, %d.%b %H:%M', point.x)+'<br><b>'+ point.series.name.substring(point.series.name.length-3, point.series.name) +': '+ point.y +'°C</b>';
				},
				"%": function (point) {
					return Highcharts.dateFormat('%a, %d.%b %H:%M', point.x)+'<br><b>'+ point.series.name.substring(point.series.name.length-3, point.series.name) +': '+ point.y +'%</b>';
				},
				"0": function (point) {
					return Highcharts.dateFormat('%a, %d.%b %H:%M', point.x)+'<br><b>'+ point.series.name.substring(0,point.series.name.length-3) +': '+ programs[point.y] +'</b>';
				}
			};
			
			$(document).ready(function() {
				
				Highcharts.setOptions({
					global: {
						useUTC: false
					},
					lang: {
						months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',  'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
						shortMonths: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
						weekdays: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota']
					}
				});
				
				options = {
					
					xAxis: {
						type: 'datetime',
						dateTimeLabelFormats: {
							day: '%e of %b',
							hour: '%H:%M<br>%d %b'
						}  
					},
					yAxis: [{ // left y axis
						title: {
							text: null
						},
						labels: {
							align: 'left',
							x: 3,
							y: 16,
							formatter: function() {
								return Highcharts.numberFormat(this.value, 0);
							}
						},
						showFirstLabel: false
					}, 
					{ // right y axis
						linkedTo: 0,
						gridLineWidth: 0,
						opposite: true,
						title: {
							text: null
						},
						labels: {
							align: 'right',
							x: -3,
							y: 16,
							formatter: function() {
								return Highcharts.numberFormat(this.value, 0);
							}
						},
						showFirstLabel: false
					}],
					
					legend: {
						labelFormatter: function() {
							var name = this.name;
							name = name.substring(0, name.length-3);
							return name;
						}
					},
					
					tooltip: {
						formatter: function() {
							var name = this.series.name;
							return tooltipFormatter[name.substring(name.length-2,name.length-1)](this);
						}
					},
					plotOptions: {
						series: {
							cursor: 'pointer',
							marker: {
								enabled: false,
								lineWidth: 1
							}
						}
					},
					chart: {
						type: 'line',
						zoomType: 'x'
					}
				};
				
				
				UpdateChart();							
				
				$('#DateFrom').datepicker({
					firstDay: 1,				
					dateFormat: "yy-mm-dd",
					dayNamesMin: ['Nie', 'Pon', 'Wto', 'Sro', 'Czw', 'Pia', 'Sob'],
					monthNames: [ 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',  'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
					onClose: function () {
						this.value+=' 00:00:00';
					}		
				});
	
				$('#DateTo').datepicker({
					firstDay: 1,
					dateFormat: "yy-mm-dd",
					dayNamesMin: ['Nie', 'Pon', 'Wto', 'Sro', 'Czw', 'Pia', 'Sob'],
					monthNames: [ 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',  'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
					onClose: function () {
						this.value+=' 23:59:59';
					}
				});
	
				$('#SubmitButton').click(function() {
					UpdateChart();
				});
			});
				
		</script>
		
	</head>
	<body>
		<center><p style="font-family: Arial">From: <input type="text" id="DateFrom" readonly="True"> To: <input type="text" id="DateTo" readonly="True"> <input type="button" id="SubmitButton" value="Update!" /></p></center>
		<div id="container" style="width: 1200px; height: 600px; margin: 0 auto"></div>	
	</body
</html>

4. Plik PHP do umieszczenia na Raspberry Pi, który odpytuje serwer telnetowy o aktualne dane. Wykorzystuje on bibliotekę telnet.class.php. Komendy do wykonania umieszczane są w pliku comm.txt

<?php
$time = microtime();
$time = explode(' ', $time);
$time = $time[1]+$time[0];
$start = $time;
?>
	
<html>
    <head>
	<title>Testing vcontrol</title>
    </head>
<body>
	
<?php
echo "<p>Talking with your Vitodens:</p>";
$commands = file ("comm.txt");
require_once "Telnet.class.php";
$telnet = new Telnet('localhost', 83);
$connection=$telnet->connect();
if ($connection==1) {
	foreach($commands as $c) {
		echo $c;
		echo ": ";
		$result=$telnet->exec("g ".rtrim($c));
		echo $result;
		echo "<br>";
	};
}
else {
	echo "ERROR: Unable to connect to vitalk<br>";
}
$telnet->disconnect();
	
//generating the footer with processing time
$time = microtime();
$time = explode(' ', $time);
$time = $time[1]+$time[0];
$finish  = $time;
$total_time = round(($finish - $start),4);
echo "Page genrated in ".$total_time." seconds.";
?>
	
</body>
</html>

Dużo tych technicznych informacji… Mam jednak nadzieję, że zamieszczone przeze mnie pliki zaoszczędza komuś sporo czasu. Przebijanie się przez cały ten gąszcz kosztował mnie kilka długich dni ;)

Co już działa? Wykresy. Nie mam zamiaru utrzymywać ich na zawsze, chciałbym jednak przyjrzeć się pracy mojego pieca i zrozumieć, co od czego zależy. Dziś wiem, że temperatura woda w baniaku wody użytkowej spada od godziny 24:00 do 06:40 z 58 na 52 stopnie Celcjusza. Chcę sprawdzić, o ile szybciej schłodzi się przy włączonej pompie obiegu ciepłej wody…. Czas na tweaking ;)

Wykres

Co jest do zrobienia?


1. Wizualizacja i sterowanie przez WWW. Dostęp do danych już jest, trzeba to tylko graficznie ułożyć…
2. Komunikacja z PLC. Do rozgryzienia czy przez http (bibliotekę już mam przetestowaną do innych rzeczy), czy bezpośrednio przez telnet.

OK, wystarczy. Zapraszam do komentowania i… zgłaszania usprawnień.