Kotinäyttö

PenaPedia
Siirry navigaatioonSiirry hakuun
Lähes alkeista aloitettu uudestaan

Tätä on nyt muutamina viimeviikkoina herätelty henkiin taas. Olen hukannut viimeisimmän uusimman version, joten liikkeelle lähdetään hyvin varhaisista alkeista.

Tällä kertaa grafiikkaa ei piirretä minkään kirjaston avustuksella, vaan natiiveilla html:n canvas-työkaluilla (tai sitten ei ollenkaan).

Nykyään osaan myös paremmin jaotella mitä tehdään palvelinpuolella ja mitä front-endin puolella selaimen javascriptillä, aikaisempaan sekasotkuun verrattuna.

Hankkeen tila

01/12/2025

Tämä on edistynyt nyt taas vaihteeksi. Kaikki aiemmin grafiikan piirtoon käytettävät kirjastot on heivattu helvettiin ja grafiikka piirretään täysin puhtaan VanillaJS:n & SVG toimesta. Erityisen hyvin lämpötilagraafi toimii nyt (KUVA PUUTTUU VIELÄ), sähkön hinnan ja kulutettujen eurojen esittämisestä olen luonnostellut ASCII-pohjaista palkkigrafiikkaa, mutta en tiedä onko se lopullinen ratkaisu.

Ehkä suurin ja tärkein läpimurto on ILP:n ohjaus. Sain aikaiseksi tilata IR-ledejä ja vastaanottimia, joiden avulla voidaan nauhoittaa kaukosäätimestä IR-signaalia ja lähettää sitä pumpulle. Sain nauhoitettua kaukosäätimestä oleellisimmat toiminnot, joita voidaan lähettää pumpulle funktion laheta_ir_komento() avulla (tässä ei vielä käytössä mutta toimivaksi trimmattu jo. HOX! Idea tai lähetysmekanismi ei ole oma ideani, mutta se on niin yksinkertainen ja nerokas ettei mikään muu ratkaisu voisi tulla kyseeseen!) TODO: Kirjoitetaan tarkempi kuvaus tuosta IR-hommasta + nauhoituksesta LIRCiä tai mitään muutakaan paskaa ei tarvita, vaan kaikki hoituu rasbianissa valmiina olevilla työkaluilla!
Tämä pumpun ohjaus on erityisesti sydämmemme asialla, koska asentaessani ilmanlämpöpumpun ilman pätevyyttä tai lupia, asensin sen niin vittumaiseen paikkaan ettei alhaalta tuvasta sisäyksikkö näe kaukosäädintä ja siksi parvelle nouseminen sitä operoimaan on aina inhottava velvoite. Toisaalta näin pienessä huoneen kokoisessa talossakaan ei kauheasti paikkoja ollutkaan valittavana.

Tällä hetkellä haasteellisinta on päättää käyttöliittymän tulevia linjauksia, miten vesi esitetään, teho, varaajan suunnitelma ja nyttemmin myös ilmanlämpöpumpun ohjaus. Sen lisäksi, että tämän avulla saadaan nyt etäkäyttö, saadaan se himmaamaan pyyntiä korkean sähkön hinnan aikaan. Pumpulle ja muulle on väkerretty textboxi, josta syötetään komentoja ja ajastuksia kaikkiin tämän eri toimintoihin. (parseri tekemättä !), olihan sekin oma työmaansa saada tuolta sivulta POST metodilla dataa palvelinpuolelle, vaan onnistui lopulta silti.

Myös ajastusten käsittelyssä on työnnetty kädet kunnolla paskaan ja luovuttu niistä timereistä joita edellisessä mallissa oli käytössä, kaikki pyritään hoitamaan itse. Myös HTTP-palvelimen kirjoittaminen on aloitettu, koska tuo pythonin moduulipalvelin vain jostain syystä vituttaa. Itse tekemällä saisin siitä sellaisen, joka avaa uuden säikeen vain sse-livestriimiä varten, tuossa nykyisessä joka requesti avataan omassa säikeessä. Lisäksi en edes ymmärrä täysin kuinka se [moduulipalvelin] toimii, joka aiheuttaa teknistä pahoinvointia.

###

  • Molemmat vesimittarit toimivat
  • ILP tehon mittaus toimii
  • Ohjelmarakenne tehdään kivuttomasti taipumaan tulevaan varttimittaukseen
  • Sähkömittarista tolpan kyljestä ei saavu enään pulsseja tänne
    • Tätä varten on maastouduttava pöpelikköön etsimään katkeaako signaali tai 5V syöttö mittauskeskukseen, jossa LDR-vastus lukee sähkömittarin lediä
  • 1-wire Lämpötila-anturit ovat aivan kadotuksissa mikä on mikäkin ja mitkä ovat niiden johdot
  • Veden esilämmitys pihalla on osittain purettu + kuopattu kiinnostuksen loputtua

Palvelimen puoli

HUOMHUOM! Näitä ei kannata edes testata, koska toimivuus on spesifisesti tässä ympäristössä ja näillä antureilla sen lisäksi että kaikki on vielä kesken + levällään. Jääkäämme seuraamaan kehitystä.


import datetime
import requests, json
import time, sys
import _thread
import RPi.GPIO as GPIO
from socketserver import ThreadingMixIn
from http.server import BaseHTTPRequestHandler, HTTPServer
import pprint
import pigpio


data = {
	'sahkonKulutus'		: {},
	'sahkonALV'   		: 1.255,
	'sahkoVero'	  		: 2.827515,
	'sahkoSiirto' 		: 4.65,
	'sahko'				: {
							'pulssien_vali'				: 0,
							'pulsseja_per_kwh'			: 10000,
							'lastTimestamp'				: 0,
							'nordPoolWaitTime'			: time.time(),
							'kokonaisPulssit'			: 0,
							'kokonaisHinta' 			: 0
						},
	'pvmTanaan' 		: datetime.date.today().strftime('%Y-%m-%d'),
	'nykyinenTunti'		: 0,
	'pvmHuomenna'		: (datetime.datetime.today()+datetime.timedelta(days=+1)).strftime('%Y-%m-%d'),
	'currTimestamp'		: time.time(),
	'cliKeskeytys'		: False,
	'meta'				: {
						'httpPyyntojaSaatu'				: 0,
						'broadcastedBytes'				: 0,
						'asiakkaitaPaikalla'			: 0
						},
	'restartTimes'      : 0,
	'ajoaikaSec'        : 0,
	'saikeetElossa'		: {
							'hengetarLastTimestamp' : time.time(),
							'mittariLastTimestamp'	: time.time()
							},
	'talletustiedosto'	: "/home/tumple/tmpfs_CACHE/55talletustiedosto.txt",
	'firstRun'			: time.time(),
	'dataStructVersion'	: 24332724,							# Jos tämän taulukon skeema muuttuu, ei vanha tiedosto ole enään käypä --> hylätään
	'viivapiirturi'		: [],
	'lammotTmp'			: {},
	'komentoTXTarea'	: "",
	'ilp'				: {
							'pulssien_vali'				: 0,
							'pulsseja_per_kwh'			: 1000,
							'lastTimestamp'				: 0,
							'kokonaisPulssit'			: 0,
							'kokonaisPulssitLast'		: 0,
							'kokonaisPulssitLastTimestamp' : time.time(),
							'kokonaisHinta'				: 0,
							'tavoitelampo'				: 21,
							'defaultIRmap'				: 'lammitys_fan4',
							'currIRmap'					: ''
						},
	'kvesi'				: {
							'pulssien_vali'			: 0,
							'lastTimestamp'			: -1,
							'kokonaisPulssit'		: 0,
							'kokonaisPulssitLast'	: 0
						},
	'lvesi'				: {	'pulssien_vali'			: 0,
							'lastTimestamp'			: 0,
							'varaajanKap'			: 99000, 	# 99000 pulssia = 150 litraa
							'varaajanHavioteho' 	: 0.15,		# kW
							'varaajanTeho'			: 1.8,		# kW
							'varaajanStatus'		: 99000,
							'kylma_vesi_c'			: 4,
							'kokonaisPulssit'		: 0,
							'kokonaisPulssitLast' 	: 0,
							'lampimanVedenArvo'		: 0,
							'varaajanSuunnitelma'	: {}
							},
	'vedenKayttokerrat'	: [],
	'sahkonHinta'	 	: {},
	'lastLogs' 			: [],
	'errorLogs'			: []
}


#28-3c05f6482521  28-3c12e381c7a4  28-3ca9f648c262  28-3cf5f648648b
devices = {						#/sys/bus/w1/devices
        'huone'             : '/sys/bus/w1/devices/28-3cf5f648648b/w1_slave', 
        'parvi'             : '/sys/bus/w1/devices/28-3c05f6482521/w1_slave', 
        'ulko'              : '/sys/bus/w1/devices/28-3c12e381c7a4/w1_slave',
        'sisa_puhallus'     : '/sys/bus/w1/devices/28-3ca9f648c262/w1_slave', 
        'kierron_meno'      : '/sys/bus/w1/devices/28-3ce10457c47b/w1_slave',
        'kierron_paluu'     : '/sys/bus/w1/devices/28-3ce1045723c3/w1_slave',
        'ponton_lampotila'  : '/sys/bus/w1/devices/28-3ce10457e032/w1_slave'
}

irCodes = {
	'lammitys_fan1'			: {},
	'lammitys_fan2'			: {},
	'lammitys_fan3'			: {},
	'lammitys_fan4'			: {},
	'lammitys_fan5'			: {},
	'lammitys_fan6'			: {},
	'viilennys_fan5'		: {},
	'viilennys_fan4'		: {},
	'viilennys_fan3'		: {},
	'viilennys_fan2'		: {},
	'viilennys_fan1'		: {},
	'sammutus'				: {},
	'kosteuden_poisto'		: {}
}


def carrier(gpio, frequency, micros):
   """
   Generate carrier square wave.
   """
   wf = []
   cycle = 1000.0 / frequency
   cycles = int(round(micros/cycle))
   on = int(round(cycle / 2.0))
   sofar = 0
   for c in range(cycles):
      target = int(round((c+1)*cycle))
      sofar += on
      off = target - sofar
      sofar += off
      wf.append(pigpio.pulse(1<<gpio, 0, on))
      wf.append(pigpio.pulse(0, 1<<gpio, off))
   return wf

def laheta_ir_komento(code):
	GAP_MS     = 100
	GAP_S      = GAP_MS  / 1000.0
	pi = pigpio.pi() # Connect to Pi.

	if not pi.connected:
		exit(0)

	pi.set_mode(pinnit['irTx'], pigpio.OUTPUT) # IR TX connected to this GPIO.
	pi.wave_add_new()
	emit_time = time.time()
	#code = str
	marks_wid = {}
	spaces_wid = {}
	wave = [0]*len(code)
	for i in range(0, len(code)):
		ci = code[i]
		if i & 1: # Space
			if ci not in spaces_wid:
				pi.wave_add_generic([pigpio.pulse(0, 0, ci)])
				spaces_wid[ci] = pi.wave_create()
			wave[i] = spaces_wid[ci]
		else: # Mark
			if ci not in marks_wid:
				wf = carrier(pinnit['irTx'], 38, ci)
				pi.wave_add_generic(wf)
				marks_wid[ci] = pi.wave_create()
			wave[i] = marks_wid[ci]
	delay = emit_time - time.time()
	if delay > 0.0:
		time.sleep(delay)
	pi.wave_chain(wave)
	while pi.wave_tx_busy():
		time.sleep(0.002)
	emit_time = time.time() + GAP_S
	for i in marks_wid:
		pi.wave_delete(marks_wid[i])
	marks_wid = {}
	for i in spaces_wid:
		pi.wave_delete(spaces_wid[i])
	spaces_wid = {}
	pi.stop() # Disconnect from Pi.

def lue_ir_codet_muistiin():
	for tiedosto in irCodes:
		with open(tiedosto, "r") as file:
			irCodes[tiedosto]['data'] = json.loads(file.read())
			file.close()


# LÄMPÖMITTARIN SÄIE KUTSUU MINUUTIN VÄLEIN TÄTÄ FUNKTIOTA
# YRITETÄÄN LUKEA USEAMPAAN KERTAAN JOS EI ONNISTU
def lampomittari_saie_1min():
	data['saikeetElossa']['mittariLastTimestamp'] = data['currTimestamp']
	retries = 6
	while lue_anturi('huone') == -999 and retries > 0:
		retries -= 1
	retries = 6
	while lue_anturi('parvi') == -999 and retries > 0:
		retries -= 1
	retries = 6
	while lue_anturi('sisa_puhallus') == -999 and retries > 0:
		retries -= 1
	retries = 6
	while lue_anturi('ulko') == -999 and retries > 0:
		retries -= 1
	return -1
	

# LÄMPÖMITTAREIDEN LUKUUN VARATTU OMA SÄIE
def lue_lampomittareita():
	logi("Lämpömittareiden luku säie käynnistyy!")
	lastTimestamp = 0 #time.time()
	while True:
		if data['cliKeskeytys']:
			return 0
		time.sleep(1)
		if lastTimestamp + 60 < time.time():
			lastTimestamp = time.time()
			lampomittari_saie_1min()
	return 0

def lue_anturi (device):
	##logi("AVATAAN LAITE: " + device)
	# huone, parvi, sisa_puhallus
	#time.sleep(1)
	try:
		with open(devices[device], 'r') as onewire:
			time.sleep(0.5)
			lines = onewire.readlines()
			onewire.close()
	except:
		logi("lue_anturi(): EI LÖYDY LAITETIEDOSTOA | device: " + device, 'err')
		#logi("lue_anturi: " + device)
		time.sleep(1)
		data['lammotTmp'][device] = -999
		return -999
	if lines == False or len(lines) != 2:
		logi("parsi_lampotila: EI PARSITTAVAA")
		print(lines)
		data['lammotTmp'][device] = -999
		time.sleep(1)
		return -999
	logi(str(lines))
	if (lines[0][-4 : len(lines[0])-1]) != 'YES':
		logi("parsi_lampotila: TEMP IS NOT VALID")
		time.sleep(1)
		data['lammotTmp'][device] = -999
		return -999
	#print(lines[0][-3 : len(lines[0])])
	lampotila = round(int(lines[1][lines[1].find("t=")+2:len(lines[1])]) / 1000, 1)
	data['lammotTmp'][device] = lampotila
	return 1

class handler(BaseHTTPRequestHandler):
	global data
	#def do_OPTIONS(self):
		#self.send_response(200)
		#self.send_header("Access-Control-Allow-Origin", "*")
		#self.send_header("Access-Control-Allow-Credentials", "true")
		#self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
		#self.send_header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Accept")  
		#self.end_headers()
	def do_GET(self):
		data['meta']['httpPyyntojaSaatu'] += 1
		#logi("GEttiä kutsutaan!")
		if self.path.endswith('/kaikki_data'): #Lähettää koko data arrayn json dumppina
			self.send_response(200)
			#self.send_header("Access-Control-Allow-Origin", "*")
			#self.send_header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Origin, Accept") 
			self.send_header('Content-type', 'application/json; charset=UTF-8')
			self.end_headers()
			output = str.encode(json.dumps(data))
			data['meta']['broadcastedBytes'] += len(output)
			self.wfile.write(output)
			logi("/kaikki_data tarjoiltu")
			return
			logi("yhteys ei katkea")
		elif self.path.endswith('/debug'):
			self.send_response(200)
			self.send_header('Content-type', 'text/plain; charset=UTF-8')
			#self.send_header("Access-Control-Allow-Origin", "*")
			self.end_headers()
			output = "<pre>"

			output += pprint.pformat(data, indent=1,sort_dicts=False)
			output += pprint.pformat(irCodes, indent=1,sort_dicts=False)
			#output += data
			output = str.encode(output)
			data['meta']['broadcastedBytes'] += len(output) # ei toimi
			self.wfile.write(output)
			return
		elif self.path.endswith('/etusivu.html'):
			self.send_response(200)
			self.send_header('Content-type', 'text/html; charset=UTF-8')
			self.send_header("Access-Control-Allow-Origin", "*")
			self.end_headers()
			f = open("etusivu.html", "r")
			output = str.encode(f.read())
			f.close()
			data['meta']['broadcastedBytes'] += len(output)
			self.wfile.write(output)
			return
		elif self.path.endswith('/livestream'):
			data['meta']['asiakkaitaPaikalla'] += 1
			self.send_response(200)
			self.send_header('Content-type', 'text/event-stream')
			self.end_headers()
			prevLastLogTimestamp = time.time()
			fullOutput = {'aiemminLahetetyt' : {}}
			firstRun = True
			while True:
				#time.sleep(1)
				if data['cliKeskeytys']:
					return
				lahetettavastLastlogit = ""
				
				
				for x in data['lastLogs']:
					if prevLastLogTimestamp < x['timestamp']:
						lahetettavastLastlogit += "\n" + x['str']
						prevLastLogTimestamp = x['timestamp']
				fullOutput = muodosta_sse_data({'lastLogStr' : lahetettavastLastlogit, 'aiemminLahetetyt' : fullOutput['aiemminLahetetyt'], 'firstRun' : firstRun})
				output = str.encode(fullOutput['outputStr'])
				data['meta']['broadcastedBytes'] += len(output)
				try:
					self.wfile.write(output)
				except:
					data['meta']['asiakkaitaPaikalla'] -= 0
					return
				time.sleep(1)
				firstRun = False
			data['meta']['asiakkaitaPaikalla'] -= 0
			return
		else:
			self.send_response(200)
			self.send_header('Content-type', 'text/html; charset=UTF-8')
			self.end_headers()
			output = "<h1>VÄÄRÄ YHTEYSPISTE</h1>"
			output = str.encode(output)
			data['meta']['broadcastedBytes'] += len(output)
			self.wfile.write(output)
			return
	def do_POST(self):
		if self.path.endswith('/post'):
			content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
			post_data = self.rfile.read(content_length) # <--- Gets the data itself
			data['komentoTXTarea'] = post_data.decode("utf-8")
			#logi(post_data)
			print(post_data)
			logi("Vastaanotettiin POST-DATA: " + str(content_length) + " merkkiä")

def muodosta_sse_data(argumentit):
	# ls lp lh
	lastLogStr = argumentit['lastLogStr']
	sseArr = {
				'sahkoDelay'		: data['sahko']['pulssien_vali'],
				'dataaLahetetty'	: data['meta']['broadcastedBytes'],
				'asiakkaitaPaikalla': data['meta']['asiakkaitaPaikalla'],
				'httpPyyntojaSaatu' : data['meta']['httpPyyntojaSaatu']				
			}
	if lastLogStr != "":
		sseArr.update({'lastLogs' 	: lastLogStr})
	if data['kvesi']['pulssien_vali'] > 0 or data['lvesi']['pulssien_vali'] > 0 or argumentit['firstRun']:
		sseArr.update({'kvesiDelay' : data['kvesi']['pulssien_vali']})
		sseArr.update({'kPulssit' 	: data['kvesi']['kokonaisPulssit']})
		sseArr.update({'lvesiDelay' : data['lvesi']['pulssien_vali']})
		sseArr.update({'varaajanStatus' : data['lvesi']['varaajanStatus']})
		sseArr.update({'varaajanKap' : data['lvesi']['varaajanKap']})
		sseArr.update({'lPulssit' 	: data['lvesi']['kokonaisPulssit']})
	if data['ilp']['pulssien_vali'] > 0:
		sseArr.update({'ilpDelay' 	: data['ilp']['pulssien_vali']})
		sseArr.update({'ilpPulssit' : data['ilp']['kokonaisPulssit']})
	#if 'ls' in argumentit['aiemminLahetetyt']:
		#if data['lammotTmp']['sisa_puhallus'] != argumentit['aiemminLaheteyt']['ls']:
	sseArr.update({'ls' : data['lammotTmp']['sisa_puhallus']})
	#if 'lp' in argumentit['aiemminLahetetyt']:
		#if data['lammotTmp']['parvi'] != argumentit['aiemminLaheteyt']['lp']:
	sseArr.update({'lp' : data['lammotTmp']['parvi']})
	#if 'lh' in argumentit['aiemminLahetetyt']:
		#if data['lammotTmp']['huone'] != argumentit['aiemminLaheteyt']['lh']:
	sseArr.update({'lh' : data['lammotTmp']['huone']})
	sseArr.update({'ul' : data['lammotTmp']['ulko']})
	#if 'komentoTXTarea' not in argumentit['aiemminLahetetyt']:
	#	if argumentit['aiemminLahetetyt']['komentoTXTarea'] != data['komentoTXTarea']:
	sseArr.update({'komentoTXTarea' : data['komentoTXTarea']})
		
	#sseArr.update({'lp' : data['lammotTmp']['parvi']})
	#sseArr.update({'lh' : data['lammotTmp']['huone']})
	
	
	output = 'data: '
	output += json.dumps(sseArr)
	output += "\n\n"
	return {'outputStr' : output, 'aiemminLahetetyt' : sseArr}
	

def paivita_aika():
	global data
	now = datetime.datetime.now()
	data['pvmTanaan'] = datetime.date.today().strftime('%Y-%m-%d')
	data['nykyinenTunti'] = now.hour
	data['currTimestamp'] = time.time()

def hae_sahkonhinta(paiva = datetime.date.today().strftime('%Y-%m-%d')):


	if paiva in data["sahkonHinta"]:
		logi(paiva + " HINTA ON JO LISTALLA", 'err')
		return -1
	if time.time() < data['sahko']['nordPoolWaitTime']:
		logi("ESTETTY HINTOJEN HAKU LIIAN USEIN", "err")
		return -1
	data['sahko']['nordPoolWaitTime'] = time.time() + (60 * 5)
	headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36'}
	osoite = "https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices?date=" + paiva + "&market=DayAhead&deliveryArea=FI&currency=EUR"
	try:
		url = requests.get(osoite, headers = headers)
	except:
		logi("SÄHKÖN HINTAA EI SAA HAETTUA: requests.get()", 'err')
		return -1
	
	try:
		vastaus = json.loads(url.text)
	except:
		logi("SÄHKÖN HINTAA EI SAA HAETTUA: json.loads()", 'err')
		return -1
	if 'deliveryDateCET' not in vastaus:
		logi("deliveryDateCET ei löydy vastauksesta", 'err')
		return -1
	if vastaus["deliveryDateCET"] != paiva:
		logi("deliveryDateCET ei täsmää päivämäärään", 'err')
		return -1
	data['sahkonHinta'][paiva] = {'hinta' : [], 'kulutus' : [], 'ilpKulutus' : [], 'kylmavesi' : [], 'kuumavesi' : [] }
	tunti = 0
	for x in vastaus["multiAreaEntries"]:
		hinta = x["entryPerArea"]["FI"]
		hinta = round((((hinta / 10) + data['sahkoVero']) * data["sahkonALV"]) + data["sahkoSiirto"], 2)
		#print(hinta)
		data['sahkonHinta'][paiva]['hinta'].append(hinta)
		data['sahkonHinta'][paiva]['kulutus'].append(0)
		data['sahkonHinta'][paiva]['ilpKulutus'].append(0)
		data['sahkonHinta'][paiva]['kylmavesi'].append(0)
		data['sahkonHinta'][paiva]['kuumavesi'].append(0)
		
	
	return 1

def saatana(e):
	return e['hinta']


def suunnittele_varaaja():
	#hinnat.append({'tunti' : x, 'hinta' : random.randrange(3, 49)})
	
	hinnat = []
	suunnitelma = {}
	#Jos edes tämän päiväisiä sähkönhintoja ei ole, niin turha tehdä mitään
	if data['pvmTanaan'] not in data["sahkonHinta"]:
		return 0
	
	
	for x in range(data['nykyinenTunti'], 24):
		hinnat.append({'tunti' : x, 'hinta' : data['sahkonHinta'][data['pvmTanaan']]['hinta'][x]})
		print(x)
	
	huominen = (datetime.datetime.today()+datetime.timedelta(days=+1)).strftime('%Y-%m-%d')
	if huominen in data["sahkonHinta"]:
		for i in range(0, len(data["sahkonHinta"][huominen]['hinta'])):
			hinnat.append({'tunti' : i + x, 'hinta' : data['sahkonHinta'][huominen]['hinta'][i]})
	#print(hinnat)
	
	myMin = min(d['hinta'] for d in hinnat)
	#print(myMin)


	kapasiteetti = data['lvesi']['varaajanKap']
	lammitysteho = data['lvesi']['varaajanTeho']
	havioteho =  0.5 #data['lvesi']['varaajanHavioteho']
	haviotehoPulssia = ((havioteho * 60) / (85 - 4) / 4.2) * 11 * 60 * 60
	print(haviotehoPulssia)
	time.sleep(1)
	status = data['lvesi']['varaajanStatus']

	heatPulsesPerHour = ((lammitysteho * 60) / (85 - 4) / 4.2) * 11 * 60 * 60
	lammitysTarvePulssia = kapasiteetti - status
	lammitysTarveTuntia = round(lammitysTarvePulssia / heatPulsesPerHour)+1

	hinnat = sorted(hinnat, key=saatana)

	i = 0
	for x, val in enumerate(hinnat):
		print(i)
		tila = False
		status = data['lvesi']['varaajanStatus'] - haviotehoPulssia * (val['tunti'] - data['nykyinenTunti'])
		#status -= 500
		#or x['hinta'] < myMin * 1.1
		if i <= lammitysTarveTuntia or val['hinta'] < myMin * 1.2:
			tila = True
			status += heatPulsesPerHour
			#i += 1
		if status > kapasiteetti:
			status = kapasiteetti
		tmpArr = {'hinta' : val['hinta'], 'tunti' : val['tunti'], 'status' : round(status), 'tila' : tila}
		suunnitelma[val['tunti']] = tmpArr
		i += 1
	
	data['lvesi']['varaajanSuunnitelma'] = suunnitelma
	#print(pprint.pformat(suunnitelma, indent=1,sort_dicts=False))

def vesimittari(channel):
	tmpAika = time.time()			# Keskeytysten ajankohta on tarkka, joten paivita_aika() tarjoamia lukuja ei voi täällä käyttää
	if channel == pinnit['sahko']:	# SÄHKÖMITTARI
		logi("SÄHKÖMITTARIPULSSI")
		data['sahko']['pulssien_vali'] = tmpAika - data['sahko']['lastTimestamp']
		data['sahko']['lastTimestamp'] = tmpAika
		data['sahkonHinta'][data['pvmTanaan']]['kulutus'][data['nykyinenTunti']] += 1
		data['sahko']['kokonaisPulssit'] += 1
		return 0
	if channel == pinnit['ilpSahko']:	# ILP MITTARI
		data['ilp']['pulssien_vali'] = tmpAika - data['ilp']['lastTimestamp']
		data['ilp']['lastTimestamp'] = tmpAika
		data['sahkonHinta'][data['pvmTanaan']]['ilpKulutus'][data['nykyinenTunti']] += 1
		data['ilp']['kokonaisPulssit'] += 1
		#logi("ILPPULSSI")
		return 0
	if channel == pinnit['kvesi']:	# KYLMÄN VEDEN MITTARI kokonaisPulssit
		data['kvesi']['pulssien_vali'] = tmpAika - data['kvesi']['lastTimestamp']
		data['kvesi']['lastTimestamp'] = tmpAika
		data['sahkonHinta'][data['pvmTanaan']]['kylmavesi'][data['nykyinenTunti']] += 1
		data['kvesi']['kokonaisPulssit'] += 1
		#logi("VESIPULSSI")
		return 0
	if channel == pinnit['lvesi']:	# KUUMAN VEDEN MITTARI
		data['lvesi']['pulssien_vali'] = tmpAika - data['lvesi']['lastTimestamp']
		data['lvesi']['lastTimestamp'] = tmpAika
		data['sahkonHinta'][data['pvmTanaan']]['kuumavesi'][data['nykyinenTunti']] += 1
		data['lvesi']['varaajanStatus'] -= 1
		data['lvesi']['kokonaisPulssit'] += 1
		#logi("KUUMAVESIPULSSI")
		return 0

#Kun virtaus pysähtyy, ei keskeytystä enään tule = ei jätetä edellistä taajuuslukemaa päälle
def pysayta_mittari(asap = False):
	global data
	# Huolehditaan käynnistettäessä luvut nollille varmasti
	if asap:
		data['kvesi']['pulssien_vali'] = -1
		data['lvesi']['pulssien_vali'] = -1
	
	if data['currTimestamp'] - data['kvesi']['lastTimestamp'] > 1:
		data['kvesi']['pulssien_vali'] = -1
	if data['currTimestamp'] - data['lvesi']['lastTimestamp'] > 1:
		data['lvesi']['pulssien_vali'] = -1
	if data['currTimestamp'] - data['ilp']['lastTimestamp'] > 360:
		data['ilp']['pulssien_vali'] = -1
	if data['currTimestamp'] - data['sahko']['lastTimestamp'] > 1:
		data['sahko']['pulssien_vali'] = 0
	
	#if time.time() - data['sahko']['lastTimestamp'] > 1:
	#    data['sahko']['pulssien_vali'] = False
		
def siivoa(toiminto):
	if toiminto == "siivoaLastlog":
		if len(data['lastLogs']) < 30:
			#print("ALLE 10")
			return 0
		#print("yli 10")
		while len(data['lastLogs']) > 20:
			data['lastLogs'].pop(0)
		return 0
	if toiminto == "siivoaViivapiirturi":
		if len(data['viivapiirturi']) < 60 * 24:
			return 0
		while len(data['viivapiirturi']) > 60 * 24:
			data['viivapiirturi'].pop(0)
		return 0
	if toiminto == "siivoaVedenKayttokerrat":
		if len(data['vedenKayttokerrat']) < 50:
			return 0
		while len(data['vedenKayttokerrat']) > 50:
			data['vedenKayttokerrat'].pop(0)
		return 0


def kodin_hengetar():
	global data
	logi("Kodin hengetär täällä!")
	data['saikeetElossa']['hengetarLastTimestamp'] = data['currTimestamp']
	tt = {'1minWait': 0, '5minWait' : 0, '15minWait': 0}
	while True:
		if data['cliKeskeytys']:
			return 0

		paivita_aika()
		#logi("Kodin hengetär täällä!")
		pysayta_mittari()
		if tt['1minWait'] < data['currTimestamp']:
			#logi("1 min sykli aktivoitui!")
			ajastus_1min()
			tt['1minWait'] = data['currTimestamp'] + 60
			continue
		if tt['5minWait'] < data['currTimestamp']:
			logi("5 min sykli aktivoitui!")
			ajastus_5min()
			tt['5minWait'] = data['currTimestamp'] + 60 * 5
			continue
		if tt['15minWait'] < data['currTimestamp']:
			logi("15 min sykli aktivoitui!")
			ajastus_15min()
			tt['15minWait'] = data['currTimestamp'] + 60 * 15
			continue
		kirjaa_veden_kayttokerrat()
		time.sleep(0.005)
	return 0


def kirjaa_veden_kayttokerrat():
	if data['kvesi']['pulssien_vali'] > 0: #Jos vettä kulutetaan
		if 'vedenKayttokerratTmp' not in data: #ja tmpmuuttujaa ei ole vielä olemassa
			data['vedenKayttokerratTmp'] = {'alku' : data['currTimestamp'], 'kpulssitStart' : data['kvesi']['kokonaisPulssit'], 'lpulssitStart' :  data['lvesi']['kokonaisPulssit']}
		return 0
	#Tähän päästään vain jos mittarit eivät rekisteröi virtausta
	# Eli hana on jo suljettu, tmpmuuttuja on luotu ylempänä
	if 'vedenKayttokerratTmp' in data:
		# Alle 10 pulssin häiriöitä ei rekisteröidä ollenkaan
		if data['kvesi']['kokonaisPulssit'] - data['vedenKayttokerratTmp']['kpulssitStart'] < 10:
			del data['vedenKayttokerratTmp']
			logi("HARHA VEDENKÄYTÖSSÄ")
			return 0
		#Kirjataan veden käyttökerta lopulliseen listaan
		data['vedenKayttokerrat'].append({
			'alku' 		: round(data['vedenKayttokerratTmp']['alku']),
			'loppu' 	: round(data['currTimestamp']),
			'kpulssit' 	: data['kvesi']['kokonaisPulssit'] - data['vedenKayttokerratTmp']['kpulssitStart'],
			'lpulssit' 	: data['lvesi']['kokonaisPulssit'] - data['vedenKayttokerratTmp']['lpulssitStart']})
		del data['vedenKayttokerratTmp'] #Poistetaan tmpmuuttuja, josta seuraavalla kerralla havaitaan uusi vedenkäyttösessio


def toimeenpane_komento_txtarea():
	if len(data['komentoTXTarea']) < 1:
		return 0
	arr = data['komentoTXTarea'].split("\n")
	return 0
	for k in arr:
		if arr[k][0] == "#":
			continue
	
	return 1


def ajastus_1min():
	global data
	# Otetaan huominen päivä palvelimen päästä, koska en nyt tähän hänään keksi miten tuo hoidetaan JS:llä
	data['pvmHuomenna'] = (datetime.datetime.today()+datetime.timedelta(days=+1)).strftime('%Y-%m-%d')
	data['saikeetElossa']['hengetarLastTimestamp'] = data['currTimestamp']
	#HINNAT TÄNÄÄN:
	if data['pvmTanaan'] not in data['sahkonHinta']:
		if hae_sahkonhinta(data['pvmTanaan']) > 0:
			logi("Haettiin tämän päivän hinnat!")
	#HINNAT HUOMENNA:
	if data['nykyinenTunti'] > 13:
		huominen = (datetime.datetime.today()+datetime.timedelta(days=+1)).strftime('%Y-%m-%d')
		if huominen not in data['sahkonHinta']:
			if (hae_sahkonhinta(huominen) > 0):
				logi("Haettiin huomisen hinnat!")
	# Lisätään varaajaan joka minuutti lämmitystehon mukainen määrä pulsseja
	# Ellei varaaja ole jo täynnä
	if data['lvesi']['varaajanStatus'] < data['lvesi']['varaajanKap']:
		heatDiff = 85 - data['lvesi']['kylma_vesi_c']
		heatPulsesPerMin = ((data['lvesi']['varaajanTeho'] * 60) / heatDiff / 4.2) * 11 * 60
		data['lvesi']['varaajanStatus'] += round(heatPulsesPerMin)

	
	#Kuluvan minuutin keskimääräinen virtaus tai teho
	# Lisätään vain jos muutoksia on tapahtunut
	tmpArr = {}
	if data['ilp']['kokonaisPulssit'] != data['ilp']['kokonaisPulssitLast']:
		tmpArr['ilp'] = data['ilp']['kokonaisPulssit'] - data['ilp']['kokonaisPulssitLast']
		data['ilp']['kokonaisPulssitLast'] = data['ilp']['kokonaisPulssit']
		#Summataan nykyisen tunnin sähkön hinnalla kokonaishintaa myös tällä tavalla:
		data['ilp']['kokonaisHinta'] += round((tmpArr['ilp'] / 1000) * data['sahkonHinta'][data['pvmTanaan']]['hinta'][data['nykyinenTunti']])
	if data['kvesi']['kokonaisPulssit'] != data['kvesi']['kokonaisPulssitLast']:
		tmpArr['k'] = data['kvesi']['kokonaisPulssit'] - data['kvesi']['kokonaisPulssitLast']
		data['kvesi']['kokonaisPulssitLast'] = data['kvesi']['kokonaisPulssit']
	if data['lvesi']['kokonaisPulssit'] != data['lvesi']['kokonaisPulssitLast']:
		tmpArr['l'] = data['lvesi']['kokonaisPulssit'] - data['lvesi']['kokonaisPulssitLast']
		data['lvesi']['kokonaisPulssitLast'] = data['lvesi']['kokonaisPulssit']
		# data['lvesi']['lampimanVedenArvo'] += Nyut ei pää toimi näin hankalille hommille
	
	# LÄMPÖMITTARIN LUKEMAT MYÖS VIIVAPIIRTURIIN
	if 'sisa_puhallus' in data['lammotTmp']:
		if data['lammotTmp']['sisa_puhallus'] != -999:
			tmpArr['ls'] = data['lammotTmp']['sisa_puhallus']
	if 'parvi' in data['lammotTmp']:
		if data['lammotTmp']['parvi'] != -999:
			tmpArr['lp'] = data['lammotTmp']['parvi']
	if 'huone' in data['lammotTmp']:
		if data['lammotTmp']['huone'] != -999:
			tmpArr['lh'] = data['lammotTmp']['huone']
	if 'ulko' in data['lammotTmp']:
		if data['lammotTmp']['ulko'] != -999:
			tmpArr['ul'] = data['lammotTmp']['ulko']
	
	
	
	#if len(tmpArr) < 1:
	#	tmpArr = False
	data['viivapiirturi'].append(tmpArr)
	suunnittele_varaaja()
	toimeenpane_komento_txtarea()
	return 0

def ajastus_5min():
	siivoa("siivoaLastlog")
	siivoa("siivoaViivapiirturi")
	siivoa("siivoaVedenKayttokerrat")
	return 0

def ajastus_15min():
	talleta_tiedostoon()
	return 0





def logi(txt, error = ""):
	logStr = "[" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "] " + txt
	logArr = {"timestamp" : round(time.time()), "str" : logStr }
	print(logStr)
	data['lastLogs'].append(logArr)
	if error == "err":
		data['errorLogs'].append(logArr)
	time.sleep(1/1000)

def talleta_tiedostoon():
	data['ajoaikaSec'] += time.time() - start
	logi("tallennetaan data")
	file = open(data['talletustiedosto'], "w")
	file.write(json.dumps(data))
	file.close()
	logi("Tallennettiin tiedosto")


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    logi("ThreadedHTTPServer käynnistyy")
    """Handle requests in a separate thread."""

def create_server():
    port = 8080
    global data
    httpd = ThreadedHTTPServer(("0.0.0.0", port), handler)
    logi("HTTP KUUNTELEE PORTISSA " + str(port))
    httpd.serve_forever()


# TÄTÄ KÄYTETÄÄN VAIN DEBUGGAUKSEEN/SIMULOINTIIN, JOS TODELLISTA SIGNAALIA EI TULE
def keskeytyksien_simulointi():
	logi("Simulointisäie käynnistyi!")
	time.sleep(5)
	while True:
		if data['cliKeskeytys']:
			return 0
		vesimittari(17)
		time.sleep(0.1)

#print(hae_sahkonhinta())

#hae_sahkonhinta("2024-11-30")


pinnit = {'sahko'		: 23,	# sähkömittari
		'ilpSahko'		: 27,	# ILP mittari
		'lvesi'			: 26,	# KUUMAVESI
		'kvesi'			: 24,	# KYLMÄVESI
		'irTx'			: 19	# IR LÄHETYS
}


GPIO.setmode(GPIO.BCM)
# SÄHKÖMITTARI
GPIO.setup(pinnit['sahko'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pinnit['sahko'], GPIO.FALLING, bouncetime=2)
GPIO.add_event_callback(pinnit['sahko'], vesimittari)

# ILP MITTARI
GPIO.setup(pinnit['ilpSahko'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pinnit['ilpSahko'], GPIO.FALLING, bouncetime=30)
GPIO.add_event_callback(pinnit['ilpSahko'], vesimittari)


# KUUMAN VEDEN MITTARI
GPIO.setup(pinnit['lvesi'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pinnit['lvesi'], GPIO.FALLING, bouncetime=2)
GPIO.add_event_callback(pinnit['lvesi'], vesimittari)

# KYLMÄN VEDEN MITTARI

GPIO.setup(pinnit['kvesi'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(pinnit['kvesi'], GPIO.FALLING, bouncetime=2)
GPIO.add_event_callback(pinnit['kvesi'], vesimittari)

#sys.exit()

if 'skip_file' in sys.argv:
	data['dataStructVersion'] = time.time()

try: #Jos data array on aiemmin dumpattu tiedostoon talteen
	with open(data['talletustiedosto'], "r") as file:
		logi("TIEDOSTO LÖYTYI")
		dataTmp = json.loads(file.read())
		file.close()
		if dataTmp['dataStructVersion'] == data['dataStructVersion']:
			data = dataTmp
			del dataTmp
			logi("TIEDOSTO ON VALIDI")
		else:
			del dataTmp
			logi("----- TIEDOSTO ON EPÄVALIDI --------")
except:
	logi("Ei tiedostoa")

data['cliKeskeytys'] = False
data['restartTimes'] += 1
paivita_aika()
pysayta_mittari(True) #pysayta mittari ilman timeouttia == asap 

data['saikeetElossa']['hengetarLastTimestamp'] = data['currTimestamp']
_thread.start_new_thread(kodin_hengetar, tuple())

data['saikeetElossa']['mittariLastTimestamp'] = data['currTimestamp']
_thread.start_new_thread(lue_lampomittareita, tuple())

_thread.start_new_thread(create_server, tuple())

#_thread.start_new_thread(keskeytyksien_simulointi, tuple())

#print(data)

lue_ir_codet_muistiin()


start = time.time()
try:
	while True:
		paivita_aika()
		time.sleep(1)
		if ((data['saikeetElossa']['hengetarLastTimestamp'] + (60 * 2)) < data['currTimestamp']) or (data['saikeetElossa']['mittariLastTimestamp'] + (60 * 2) < data['currTimestamp'] ):
			data['cliKeskeytys'] = True
			time.sleep(4)
			logi("--------- JOMPI KUMPI SÄIE ON KUOLLUT ------- ", "err")
			sys.exit()
except KeyboardInterrupt:
	data['cliKeskeytys'] = True
	time.sleep(3)
	talleta_tiedostoon()
	logi("SAMMUTETAAN")

Front-end puoli


<!DOCTYPE html>
<html lang="fi">
<head>
<title>aaa</title>
<style>
.sin {
	color: blue;
}
.pun {
	color: red;
}
.vih {
	color: green;
}
.mag {
	color: magenta;
}
.pystyyn {
	transform: rotate(-180deg);
	writing-mode: vertical-rl;
	font-size: xx-small;
	/*font-family: Arial, monospace;*/
}
</style>
</head>
<body>
<div style="text-align: center;" id="tehoJaHinta"></div>
<div style="border: 1ox solid black;">
	<span id="ilp"></span>
	<span id="ilpKokonaisKulutus"></span>
</div>
<div style="border: 1px dotted pink; background-color: #e8cce0;">
	Lämmöt:	<span id="ls"></span><span id="lp"></span><span id="lh"></span><span id="ul"></span><br>
	<details><summary>Näytä ajastukset ja komennot</summary>
		<form method="post"  id="form-propt" name="form-propt" onsubmit="submitFormFetch(); return false;" >
			<textarea rows="10" cols="50" id="komentoTXTarea" name="komentoTXTarea" style="width: 85%; height: 100px;"></textarea><br>
			<input type="submit" id="submit" name="submit" value="submit">
		</form>
	</details>
</div>
<div style="float: right;">
	<div style="border: 1px dotted blue;">
		<h3>Veden kokonaiskulutus</h3>
		<div id="vedenKokonaiskulutus"></div>
		<canvas id="varaajan_tila" width="200" height="30" style="border:1px solid #d3d3d3; "></canvas>
	</div>
	<div style="border: 1px solid black;" id="ajonaikaisetTiedot"></div> <!-- overflow: auto; display: block; -->
</div>



<div id="kulutusASCII" style=" padding-bottom: 20px;flex-direction: row;height: 400px; width: 95%;  display: flex; font-size: xx-small;overflow: auto; font-family:  monospace;  white-space: pre;"></div>

<div id="kulutusASCIIpaivittain" style=" padding-bottom: 20px;flex-direction: row;height: 400px; width: 95%;  display: flex; font-size: xx-small;overflow: auto; font-family:  monospace;  white-space: pre;"></div>


<!-- <div id="sahkonHintaSVG" style="border: 1px solid black; width: 700px; height: 300px;"></div> -->
<div>
	<button onclick="lampoSVGrange(60)">60 min</button>
	<button onclick="lampoSVGrange(60*8)">8 h</button>
	<button onclick="lampoSVGrange(60*24)">24 h</button>
	
</div>
<div id="viivapiirturiSVG" style="border: 1px solid black; width: 700px; height: 300px;"></div>
<!--<div> <canvas id="lampotilaCanvas" width="600" height="150" style="border:1px solid #000000;"></canvas></div>-->
<div> <canvas id="sahkonHintaCanvas" width="600" height="150" style="border:1px solid #000000;"></canvas></div>


<div style="border: 1px solid black; overflow: scroll; height: 150px; width: 600px;"><h3>Veden käyttökerrat</h3><p id="vedenKayttokerrat"></p></div>

<div> <canvas id="vedenKayttokerratCanvas" width="600" height="150" style="border:1px solid #000000;"></canvas></div>
<div style="margin: 10px; padding: 10px; border: 1px solid black;"><textarea style="width: 80%; height: 400px;" rows="20" cols="20" id="lastLogsArea"></textarea></div>



</body>
</html>


<script>

"use strict";
var sets = {'pulsseja_per_kwh' : 0,
			'ilp_pulsseja_per_kwh' : 0,
			'nykyinenHinta' : 0,
			'pvmTanaan' : 0,
			'nykyinenTunti' : 0};

var pubs = {'nykyinenTunti' : 0,
			'lampoSVGslice' : 60
			};

var allData = {};

function lampoSVGrange(mins) {
	pubs['lampoSVGslice'] = mins;
	piirra_lampotila_svg(allData['viivapiirturi']);
}

function piirra_sahkon_hinta_svg(dataArr) {
	let str = "";
	str += "<svg width='700' height='300' xmlns='http://www.w3.org/2000/svg'>\ṇ";
	let i = 0, pituus = 0, lMargin = 40, rSet = [];
	console.log(dataArr);
	dataArr = dataArr[0];
	pituus = dataArr.length;
	let width = 700;
	let step = (width - lMargin) / pituus;
	let keskihinta = dataArr.reduce((a, b) => a + b) / pituus;

	let hMax = Math.max(...dataArr);

	let hMin = Math.min(...dataArr);
	
	let hFact =  270 / (hMax);
	
	let yMax = 280;
	rSet['keskihinta'] = (yMax - (hFact * keskihinta)).toFixed(0);
	for (i = 0; i < pituus; i++) {
		rSet['xPos'] = i*step + lMargin;

		rSet['value'] = ((hFact * dataArr[i])).toFixed(0);
		rSet['yPos'] = yMax - rSet['value'];
		str += `<text x="${rSet['xPos']}" y="${300}" fill="red">${i}</text>\n`;
		str += `<rect width="15" height="${rSet['value']}" x="${rSet['xPos']}" y="${rSet['yPos']}" fill="blue" />\n`;
	}
	str += `<line x1="${0}" y1="${rSet['keskihinta']}" x2="${700}" y2="${rSet['keskihinta']}" style="stroke:red;stroke-width:2" />\n`;
	str += "</svg>\n\n";
	//console.log(str);
	document.getElementById("sahkonHintaSVG").innerHTML = str;
	
}
function luo_X_label(labelArr) {
	labelArr.reverse();
	let i; let out = `<svg x="10" y="10">\n`;
	let stepSize = 700 / labelArr.length;
	
}


function get_minMax(arr) {
	const min = Math.min(...arr);
	const max = Math.max(...arr);
	const index_min = arr.indexOf(min);
	const index_max = arr.indexOf(max);
	return [arr[index_min], arr[index_max], (arr[index_max] - arr[index_min]) ];
}
function luo_Y_label(labelArr) {
	labelArr.reverse();
	//let step = 300 / labelArr.length;
	let i; let out = `<svg y="0" >\n`;
	let stepSize = 300 / (labelArr.length - 1);
	let yOffset = stepSize;
	yOffset = 0;
	for (i = 0; i < labelArr.length; i++) {
		out += `<line x1="${0}" y1="${yOffset + (i * stepSize)}" x2="${700}" y2="${yOffset + i * stepSize}" style="stroke:gray;stroke-width:1" />\n`;
		out += `<text x="${10}" y="${yOffset + i * stepSize}" fill="red">${labelArr[i]}</text>\n`;
	}
	out += "</svg>\n";
	console.log(out);
	return out;
}
function piirra_svg_viiva(dataArr, yLabel, color) {
	let i; let out = `<svg >\n`; let cords = "";
	let stepSize = 700 / (dataArr.length );
	let mm = get_minMax(dataArr);
	let tt = get_minMax(yLabel);

	let uPerPix = 300  / (tt[2]);

	let offset = (tt[0]) * uPerPix;
	if (offset < 0) {
		offset = offset * -1;
	}
	if (tt[0] > 0) {
		//tt[2] = tt[1] - tt[0];
		offset = 0 - offset;
	}
	//offset = ;
	for (i = 0; i < dataArr.length; i++) {
		//cords += (i * stepSize).toFixed(0) + "," + (300-offset-(dataArr[i] * uPerPix).toFixed(0)) + " ";
		cords += (i * stepSize) + "," + (300-offset-(dataArr[i] * uPerPix)) + " ";
	}
	out += `<polyline style=\"fill:none;stroke:${color};stroke-width:1\" points="${cords}" />\n`;
	console.log("OFFSET: " + offset);
	console.log("uPerPix: " + uPerPix);
	return out;
}

function piirra_lampotila_svg(piirtoArr) {
	let str = Array(); let i;
	piirtoArr = piirtoArr.slice(0 - pubs['lampoSVGslice']);
	str['out'] = "<svg width='700' height='300' xmlns='http://www.w3.org/2000/svg'>\n";
	let yLabel = [-10,0,10,20,30,40];
	console.log(yLabel);
	let lh = Array(); let ls = Array(); let lp = Array(); let ul = Array(); //(20,20,20,20); //[14.5,14,15,13,15,12,10,0,5,7,8,15];
	for ( i = 0; i < piirtoArr.length; i++) {
		if (!piirtoArr[i]['lh']) {
			piirtoArr[i]['lh'] = 0;
		}
		if (!piirtoArr[i]['ls']) {
			piirtoArr[i]['ls'] = 0;
		}
		if (!piirtoArr[i]['lp']) {
			piirtoArr[i]['lp'] = 0;
		}
		if (!piirtoArr[i]['ul']) {
			piirtoArr[i]['ul'] = 0;
		}
		lh.push(piirtoArr[i]['lh']);
		lp.push(piirtoArr[i]['lp']);
		ls.push(piirtoArr[i]['ls']);
		ul.push(piirtoArr[i]['ul']);

	}
	console.log(lh);
	str['out'] += luo_Y_label(yLabel);
	str['out'] += piirra_svg_viiva(lh, yLabel, "green");
	str['out'] += piirra_svg_viiva(lp, yLabel, "blue");
	str['out'] += piirra_svg_viiva(ls, yLabel, "magenta");
	str['out'] += piirra_svg_viiva(ul, yLabel, "purple");
	//console.log(str['out']);
	str['out'] += "</svg>\n";
	document.getElementById("viivapiirturiSVG").innerHTML = str['out'];
	return 0;
}


function piirra_sahkon_hinta(hintaArr) {
	
	
	var c = document.getElementById("sahkonHintaCanvas");
	var ctx = c.getContext("2d");
	//var hintaArr = [10,5,5,0,5];
	let i = 0, pituus = 0, lMargin = 40;
	hintaArr = hintaArr[0];
	pituus = hintaArr.length
	let width = 600;
	let step = (width - lMargin) / pituus;
	let keskihinta = hintaArr.reduce((a, b) => a + b) / pituus;
	
	for (i = 0; i < pituus; i++) {
		 ctx.fillText(i + " ", i * step + lMargin , 150);
	}
	let hMax = Math.max(...hintaArr);

	let hMin = Math.min(...hintaArr);
	
	let hFact =  130 / (hMax);
	console.log("max hinta: " + hMax);
	let yMax = 140;
	
	//Keskihinnan viiva
	ctx.moveTo(0, hFact * keskihinta);
	ctx.lineTo(width, hFact * keskihinta);
	ctx.stroke();
	
	
	for (i = 0; i < pituus; i++) {
		ctx.fillRect(i * step + lMargin, yMax, 15, 0 - (hFact * hintaArr[i]));
	}
}



/*
function piirra_lampotilat(viivapiirturi) {
	var c = document.getElementById("vedenKayttokerratCanvas");
	var ctx = c.getContext("2d");
	let i = 0;
	
	for (i = 0; i < viivapiirturi.length; i++) {
		*/

function piirra_veden_kayttokerrat(piirtoArr) {
	var c = document.getElementById("vedenKayttokerratCanvas");
	var ctx = c.getContext("2d");
	let nyt = Math.round(Date.now() / 1000);
	let i;
	let arr = Array();
	let sc = piirtoArr.length, mc = 3;
	//for (i = nyt; i > (nyt - sc); i--) {
	//	arr[i] =  ctx.fillRect((i * step), yMax, step-1, 0 - (Math.floor(Math.random() * 40) + 20));
		//console.log(piirtoArr);
	let step = (600 / sc);
	let yMax = 150;
	let ilp = Array(), ls = Array();
	ilp['scaleY'] = (140 / 2.5);
	ilp['num'] = 0;
	ls['scaleY'] = 1; //(140 / 2.5);
	ls['num'] = 0;
	for (i = 0; i < piirtoArr.length; i++) {
		/*if (piirtoArr[i]['ilp']) {
			ilp['num'] = (((1000/1000)*3600)/piirtoArr[i]['ilp'])/1000;
			//console.log(ilp);
			ctx.fillRect((i * step), yMax, step-1, 0 - (ilp['num'] * ilp['scaleY']));
		}*/
		if (piirtoArr[i]['ls']) {
			ls['num'] = piirtoArr[i]['ls'];
			//console.log(ilp);
			//ctx.fillRect((i * step), yMax, step-1, 0 - (ls['num'] * ls['scaleY']));
			ctx.moveTo(((i -1) * step),  (ls['num'] * ls['scaleY']));
			ctx.lineTo((i * step),  (ls['num'] * ls['scaleY']));
			ctx.stroke();
		}
		
		
	}
}

function vedenVirtausJaKokonaiskulutus(obj) {
	let str = "";
	if (!obj['kDelay']) {
		obj['kDelay'] = 0;
	}
	if (!obj['lDelay']) {
		obj['lDelay'] = 0;
	}
	
	str += "<span style='color: blue;'>" + precise(obj['kPulssit'] / 6.6 / 60, 5) + " L ( " + (obj['kDelay'] / 11) + " L/s) </span><br>";
	str += "<span style='color: red;'>" + precise(obj['lPulssit'] / 11 / 60, 5) + " L ( " + (obj['lDelay'] / 6.6) + " L/s) </span><br>";
	

	str += "Varaajan status:"
	document.getElementById("vedenKokonaiskulutus").innerHTML = str;
	varaaja_canvas(obj['varaajanStatus'] / obj['varaajanKap']);
	

}


function nayta_veden_kayttokerrat(vesiArr) {
	//console.log(vesiArr);
	//piirra_veden_kayttokerrat(vesiArr);
	let i = 0;
	let aika, kesto, str = "", kylma = 0, kuuma = 0, prec = 0, yht = 0;
	for (i = 0; i < vesiArr.length; i++){
		kesto = precise(vesiArr[i]['loppu'] - vesiArr[i]['alku']);
		kylma = (vesiArr[i]['kpulssit'] / 6.6 / 60);
		kuuma = (vesiArr[i]['lpulssit'] / 11 / 60);
		yht = precise(kylma + kuuma);
		prec = kuuma / kylma * 100;
		//str += "<span class='sin'>" + kylma + "</span> L / <span class='pun'>" + kuuma + "</span> L " + kesto + " s <br>";
		str += " " + yht + " L " + kesto + " s <br>";
		str += "<div style='width: 100px; height: 20px; background-color: blue;'><div style='background-color: red; width: " + prec + "%;'> s</div></div>";
		//console.log(kylma);
	}
	document.getElementById("vedenKayttokerrat").innerHTML = str;

}
function varaaja_canvas(lataus) {
    //console.log("Varaajan lataus:" + lataus);
    var c = document.getElementById("varaajan_tila");
    var ctx = c.getContext("2d");
    var grd = ctx.createLinearGradient(0, 0, 200, 0);
    
    grd.addColorStop(0, "blue");
    grd.addColorStop(1, "red");

    // Fill with gradient
    ctx.fillStyle = grd;
    ctx.fillRect(0, 0, 200 * lataus, 20);
}

function piirra_sahkon_kulutus_ASCII(dataArr, varaajanSuunnitelma = null) {
	let arvot = Array(); let i; let out = ""; let out2 = ""; let yAxis; let v; let k; let mm; let w; let cFact; let style; let vari; const sets = [];
	let row = []; let hinta; let vals = [];
	//arvot = [2,2,5.2,2,2,1,2,4,2,2,2,5.2,2,2,1,2,4,2,5,9,2,1];
	arvot = dataArr['hinta'];
	console.log(varaajanSuunnitelma);
	//return 0;
	mm = get_minMax(dataArr['hinta']);
	let keskihinta = arvot.reduce((a, b) => a + b) / 24;
	w = 77;
	sets['kwScale'] = 20;
	sets['hintaScale'] = 2;
	sets['varaajanStatusScale'] = 0.2;
	sets['nykyinenTuntiStyleStr'] = '';
	cFact = w / mm[2];
	//out += "\n\n\n\n\n\n\n\n\n\n";
	yAxis = Array.from({length: 24}, (v, k) => k++ + ":00");
	//out += "<div style='border: 1px solid pink;align-self: end; display: flex;'>";
	for (i = 0; i < yAxis.length; i++) {
		vals['varaajanTila'] = "X";
		sets['nykyinenTuntiStyleStr'] = '';
		if (varaajanSuunnitelma[i]) {
			vals['varaajanTila'] = "POIS";
			if (varaajanSuunnitelma[i]['tila']) {
				vals['varaajanTila'] = "PÄÄLLÄ";
			}
			vals['varaajanStatus'] = ((varaajanSuunnitelma[i]['status'] / 99000) * 100).toFixed(0);	// 99000 = täysi varaaja
		}
		out2 += " " + yAxis[i] + "";
		console.log(pubs['nykyinenTunti']);
		if (pubs['nykyinenTunti'] == i) {
			sets['nykyinenTuntiStyleStr'] = 'background-color: yellow;';
		}
		out += "<div style='" + sets['nykyinenTuntiStyleStr'] + " margin-right: 15px; padding-right: 5px; bottom: 10px; align-self: end; border-left: 1px solid #dee0e3;  '>";
		if (arvot[i] < keskihinta) {
			vari = "vih";
		} else {
			vari = "pun";
		}
		out += "<div>";
		vals['sahkonHinta'] = dataArr['hinta'][i];
		vals['sahkonHintaRepeat'] = dataArr['hinta'][i] - (mm[0] * 0.9);
		vals['ilpHinta'] = ((dataArr['ilpKulutus'][i] / 1000) * dataArr['hinta'][i]) ;
		vals['ilpKulutus'] = dataArr['ilpKulutus'][i] / 1000;
		out += "" + `<span  class='pystyyn ${vari}'><b>` + vals['sahkonHinta'] + "</b> " + "€".repeat(vals['sahkonHintaRepeat'] * sets['hintaScale']) + "</span>";
		if (vals['ilpKulutus'] != 0) {
			out += "" + `<span  class='pystyyn mag'><b>` + (vals['ilpKulutus']).toFixed(2) + "</b> " + "#".repeat((vals['ilpKulutus']) * sets['kwScale']) + "</span>";
		}
		if (vals['ilpHinta'] != 0) {
			out += "" + `<span  class='pystyyn '><b>` + (vals['ilpHinta']).toFixed(2) + "</b> " + "€".repeat((vals['ilpHinta'])  * sets['hintaScale']) + "</span>";
		}
		if (vals['varaajanStatus']) {
			out += "" + `<span  class='pystyyn '>` + vals['varaajanStatus'] + " " + ".".repeat((vals['varaajanStatus']) * sets['varaajanStatusScale']) + "</span>";
		}

		out += "</div>";
		out += "<div>" + yAxis[i] + "</div>";
		out += "<div>" + vals['varaajanTila'] + "</div>";
		
		out += "</div>";
		//out += 
	}
	//out += "<div style='border: 1px solid pink; '>";
	//out += "<br>PÄIVÄMÄÄRÄ";
	//out += "</div>";
	//out = out + "\n" + out2;
	//console.log("OUT: " + out);
	document.getElementById("kulutusASCII").innerHTML = out;
}

function piirra_kulutustiedot_paivittain_ASCII() {
	let i, pvm, out = "", yAxis, sum;
	let paivia = allData['sahkonHinta'].length;
	//yAxis = Array.from({length: 24}, (v, k) => k++ + ":00");
	for (var key in allData['sahkonHinta']) {
		//m = key['ilpKulutus'].reduce((acc, o) => acc + parseInt(o.value), 0);
		
		//out += "<div style='" + sets['nykyinenTuntiStyleStr'] + " margin-right: 15px; padding-right: 5px; bottom: 10px; align-self: end; border-left: 1px solid #dee0e3;  '>";
		//nsole.log(sum);
	}
	
	return 0;
	document.getElementById("kulutusASCIIpaivittain").innerHTML = out;
}
	




function kasittele_kaikki_data(data) {
	//console.log(data);
	sets['pulsseja_per_kwh'] = data['sahko']['pulsseja_per_kwh'];
	sets['ilp_pulsseja_per_kwh'] = data['ilp']['pulsseja_per_kwh'];
	sets['nykyinenHinta'] = data['sahkonHinta'][data['pvmTanaan']]['hinta'][data['nykyinenTunti']];
	//console.log("nykyinen hinta: " + sets['nykyinenHinta']);
	if (data['vedenKayttokerrat']) {
		nayta_veden_kayttokerrat(data['vedenKayttokerrat']);
	}
	//data['pvmTanaan'] = '2025-01-03';
	pubs['pvmTanaan'] = data['pvmTanaan'];
	pubs['nykyinenTunti'] = data['nykyinenTunti'];
	
	
	const hintaASCII = data['sahkonHinta'][data['pvmTanaan']];
	/*if (data['sahkonHinta'][data['pvmHuomenna']]) {
		console.log("HUOMINEN LÖYTYY");
		hintaASCII = (data['sahkonHinta'][data['pvmTanaan']]).concat(data['sahkonHinta'][data['pvmHuomenna']]);
	}*/
	
	piirra_sahkon_kulutus_ASCII(hintaASCII, data['lvesi']['varaajanSuunnitelma']);
	//piirra_sahkon_hinta_svg([data['sahkonHinta'][data['pvmTanaan']]['hinta'], data['lvesi']['varaajanSuunnitelma']]);
	allData['viivapiirturi'] = data['viivapiirturi'];
	allData['sahkonHinta'] = data['sahkonHinta'];
	piirra_kulutustiedot_paivittain_ASCII();
	piirra_lampotila_svg(allData['viivapiirturi']);
	//vedenVirtausJaKokonaiskulutus(data);
	//piirra_sahkon_hinta([data['sahkonHinta'][data['pvmTanaan']]['hinta']]);
	//piirra_veden_kayttokerrat(data['viivapiirturi']);
	
	document.getElementById("komentoTXTarea").value = data['komentoTXTarea'];

}
function hae_kaikki_data() {
	let data = fetch('/kaikki_data')
	.then(response => response.json())
	.then(data =>  kasittele_kaikki_data(data));
}

function precise(x, m = 3) {
  return x.toPrecision(m);
}

hae_kaikki_data();

var source = new EventSource("/livestream");
source.onmessage = function(event) {
	const obj = JSON.parse(event.data);
	//console.log(obj);
	let teho = precise((((1000/10000)*3600)/obj['sahkoDelay'])/1000);
	let hinta = precise(teho * (sets['nykyinenHinta'] / 100));
	document.getElementById("tehoJaHinta").innerHTML = "<h1>" + teho + " kw (" + hinta + " eur/h) </h1>";
	
	if (obj['ilpDelay']) {
		let ilpTeho = precise((((1000/1000)*3600)/obj['ilpDelay'])/1000);
		let ilpHinta = precise(ilpTeho * (sets['nykyinenHinta'] / 100), 1);
		document.getElementById("ilp").innerHTML = "Ottoteho: " + ilpTeho + " kW (" + ilpHinta + " eur/h )";
		document.getElementById("ilpKokonaisKulutus").innerHTML = "Yhteensä " + (obj['ilpPulssit'] / 1000) + " kWh";
	}
		
	if (obj['lastLogs']) {
		document.getElementById("lastLogsArea").innerHTML += obj['lastLogs'];
	}
	if (obj['kvesiDelay'] || obj['lvesiDelay']) {
		vedenVirtausJaKokonaiskulutus(obj);
	}
	if (obj['lh']) {
		document.getElementById("lh").innerHTML = "Huone: <b>" + obj['lh'] + "</b> c, ";
	}
	if (obj['lp']) {
		document.getElementById("lp").innerHTML = "Parvi: <b>" + obj['lp'] + "</b> c, ";
	}
	if (obj['ls']) {
		document.getElementById("ls").innerHTML = "Ilp puhalluslämpö: <b>" + obj['ls'] + "</b> c, ";
	}
	if (obj['ul']) {
		document.getElementById("ul").innerHTML = "Ulkolämpö: <b>" + obj['ul'] + "</b> c, ";
	}
	/*if (obj['komentoTXTarea']) { // PURKKARATKAISU VÄLIAIKAISESTI
		if (document.getElementById("komentoTXTarea").value != obj['komentoTXTarea']) {
			document.getElementById("komentoTXTarea").value = obj['komentoTXTarea'];
		}
	}*/
	
}

function submitFormFetch(event) {
	var fetchForm = document.getElementById("form-propt");
	var data = new FormData(fetchForm);
	var plainFormData = Object.fromEntries(data.entries());
	//scrollToEnd();
	//tyhjaaTxt();
	//console.log(JSON.stringify(plainFormData));
	
	fetch("/post", {
		method: "POST",
		body: plainFormData["komentoTXTarea"] //JSON.stringify(plainFormData)
	})
	.then((res) => {
		//console.log(res);
		//return res.text();
	})
	.then((txt) => {
		/*alert("Submit Success");*/
	})
	.catch((err) => {
		//alert(err);
	});
	return false;
}


</script>

Katso myös

Aiheesta muualla