383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| 
 | |
| Siehe auch: Steamrock Digital Barometer
 | |
| 
 | |
| Meßwert alls 15 Minuten: 
 | |
|   Es wird in hPa gemessen mit einer Nachkommastelle
 | |
|   84 Stunden * 4 Werte je Stunde = 336 Meßwerte
 | |
| Tendenzwert über 3 Stunden
 | |
| 
 | |
| Je Zoomstufe wird ein eigener Buffer vorgehalten um ein sauberes
 | |
| Diagramm zu erhalten. Überall gilt: Je Pixel ein Meßwert.
 | |
| 
 | |
| Drucktendenz:
 | |
|   - 1 hour tendency
 | |
|   - 3 hour tendency
 | |
| 
 | |
| Verschiedene Datenquellen auswählbar:
 | |
| - intern (BME280, BMP280)
 | |
| - N2K generisch
 | |
| - Historie von 
 | |
|     - Yacht devices
 | |
|     - Capteurs?
 | |
| 
 | |
| Das Diagramm wird mit Ursprung rechts unten (x0, y0) gezeichnet,
 | |
| da die Werte in der Vergangenhait liegen, also links vom Ursprung.
 | |
| 
 | |
| Damit eine saubere Skala auf der Y-Achse erreicht wird, gibt einige
 | |
| feste Skalierungen.
 | |
| Standard: 20hPa von unten nach oben, z.B. 1015, 1020, 1025, 1030, 1035
 | |
| 
 | |
| TODO 
 | |
|   - wenn keine Luftdruckdaten vorliegen, dann eine entsprechende
 | |
|     Meldung anzeigen
 | |
| 
 | |
| """
 | |
| 
 | |
| import time
 | |
| import cairo
 | |
| from .page import Page
 | |
| 
 | |
| class Barograph(Page):
 | |
| 
 | |
|     def __init__(self, pageno, cfg, appdata, boatdata):
 | |
|         super().__init__(pageno, cfg, appdata, boatdata)
 | |
|         # Meßwert alle 15 Minuten: 
 | |
|         # 84 Stunden * 4 Werte je Stunde = 336 Meßwerte
 | |
|         self.bd = boatdata
 | |
|         if not cfg['bme280']:
 | |
|             self.source = None # keine Datenquelle!
 | |
|         else:
 | |
|             self.source = 'I' # (I)ntern, e(X)tern, None=Disabled
 | |
|         self.zoom = (1, 2, 3, 6, 12)
 | |
|         self.zoomindex = 4
 | |
|         self.series = (75, 150, 300, 600, 900)
 | |
| 
 | |
|         # Y-Axis
 | |
|         self.vmin = 0
 | |
|         self.vmax = 0
 | |
|         self.scalemin = 1000
 | |
|         self.scalemax = 1020
 | |
|         self.scalestep = 5
 | |
| 
 | |
|         # Tendenzwert über 3 Stunden
 | |
|         self.hist3 = None
 | |
|         self.hist1 = None
 | |
| 
 | |
|         self.buttonlabel[1] = '+'
 | |
|         self.buttonlabel[2] = '-'
 | |
|         self.buttonlabel[5] = 'SRC'
 | |
| 
 | |
|         self.data = None
 | |
|         self.refresh = time.time() - 30
 | |
| 
 | |
|     def handle_key(self, buttonid):
 | |
|         # TODO Serie auswählen aufgrund Zoomlevel
 | |
|         if buttonid == 1:
 | |
|             # Zoom in
 | |
|             if self.zoomindex > 0:
 | |
|                 self.zoomindex -= 1
 | |
|             self.refresh = time.time() - 30
 | |
|         elif buttonid == 2:
 | |
|             # Zoom out
 | |
|             if self.zoomindex <  len(self.zoom) - 1:
 | |
|                 self.zoomindex += 1
 | |
|             self.refresh = time.time() - 30
 | |
|         if buttonid == 5:
 | |
|             # Source
 | |
|             if self.source == 'I':
 | |
|                 self.source = 'X'
 | |
|             else:
 | |
|                 self.source = 'I'
 | |
| 
 | |
|             # Testausgabe der Datenerfassung
 | |
|             data = []
 | |
|             vmin = 9999
 | |
|             vmax = 0
 | |
|             i = self.series[self.zoomindex]
 | |
|             for value in self.bd.history['press'].series[i].get():
 | |
|                 v = value / 10
 | |
|                 data.append(v)
 | |
|                 if v < vmin and v != 0:
 | |
|                     vmin = v
 | |
|                 elif v > vmax and v != 0:
 | |
|                     vmax = v
 | |
|             print(f"Werte: vmin={vmin}, vmax={vmax}")
 | |
|             ymin, ymax, step = self.getYScale(vmin, vmax)
 | |
|             print(f"Skala: ymin={ymin}, ymax={ymax}, step={step}")
 | |
|             print(f"zoomindex={self.zoomindex}, series={self.series[self.zoomindex]}")
 | |
| 
 | |
|             hist1a = self.bd.history['press'].series[i].getvalue(3600)
 | |
|             hist1b = self.bd.history['press'].series[i].getvalue3(3600)
 | |
|             trend1 = data[0] - hist1b
 | |
|             print(f"{hist1a} / {hist1b} -> Trend1: {trend1:.1f}")
 | |
| 
 | |
|     def loadData(self):
 | |
|         """
 | |
|         Transfer data from history to page buffer
 | |
|         set y-axis according to data
 | |
|         """
 | |
|         if self.source == None:
 | |
|             # TODO hier muß noch Arbeit reingesteckt werden!
 | |
|             return False
 | |
|         self.data = []
 | |
|         self.vmin = 9999
 | |
|         self.vmax = 0
 | |
|         i = self.series[self.zoomindex]
 | |
|         for value in self.bd.history['press'].series[i].get():
 | |
|             v = value / 10
 | |
|             self.data.append(v)
 | |
|             if v < self.vmin and v != 0:
 | |
|                 self.vmin = v
 | |
|             elif v > self.vmax and v != 0:
 | |
|                 self.vmax = v
 | |
|         self.scalemin, self.scalemax, self.scalestep = self.getYScale(self.vmin, self.vmax)
 | |
|         return True
 | |
| 
 | |
|     def drawTrend(self, ctx, code, x, y, w):
 | |
|         """
 | |
|         One hour Trend
 | |
|         0: Stationary            <= 1 hPa
 | |
|         1: Rising                >1 and <= 2 hPa
 | |
|         2: Rising fast           >2 and <= 3 hPa
 | |
|         3: Rising very fast      >3 hPa
 | |
|         -1: Falling
 | |
|         -2: Falling fast
 | |
|         -3: Falling very fast       
 | |
|         """
 | |
|         trend1map = {
 | |
|             -3: "Falling_Very_Fast.png",    # > 3 hPa
 | |
|             -2: "Falling_Fast.png",         # > 2 and <= 3 hPa
 | |
|             -1: "Falling.png",              # > 1 and <= 2 hPa
 | |
|              0: "Stationary.png",           # <= +/- 1 hPa
 | |
|              1: "Rising.png",               # < -1 and >= -2 hPa
 | |
|              2: "Rising_Fast.png",          # < -2 and >= -3 hPa
 | |
|              3: "Rising_Very_Fast.png"      # < -3 hPa
 | |
|         }
 | |
| 
 | |
|         if code == 0:
 | |
|             # Pfeil horizontal rechts
 | |
|             ctx.move_to(x, y - w / 2)
 | |
|             ctx.line_to(x + w, y - w / 2)
 | |
|             ctx.draw()
 | |
|             # Position merken
 | |
|             ctx.line_to(x - w / 4, y - w)
 | |
|             ctx.line_to(x - w / 4, y)
 | |
|             ctx.line_to(x + w, y - w / 2)
 | |
|             ctx.fill()
 | |
|         elif code == 1:
 | |
|             # Pfeil schräg nach oben
 | |
|             pass
 | |
|         elif code == 2:
 | |
|             # Pfeil gerade nach oben
 | |
|             pass
 | |
|         elif code == 3:
 | |
|             # Doppelpfeil nach oben
 | |
|             pass
 | |
|         elif code == -1:
 | |
|             # Pfeil schräg nach unten
 | |
|             pass
 | |
|         elif code == -2:
 | |
|             # Pfeil gerade nach unten
 | |
|             pass
 | |
|         elif code == -3:
 | |
|             # Doppelpfeil nach unten
 | |
|             pass
 | |
| 
 | |
|     def drawWMOCode(self, ctx, code, x, y, w):
 | |
|        """
 | |
|        Three hour code
 | |
|        Code 0 to 8:
 | |
|            0: Increasing, then decreasing; athmospheric pressure the same 
 | |
|               as or higher than three hours ago
 | |
|            1: Increasing then steady; or increasing, then increasing more 
 | |
|               slowly; athmospheric pressure now higher than three hours ago
 | |
|            2: Increasing (steadily or unsteadily); athmospheric pressure
 | |
|               now higher than three hours ago
 | |
|            3: Decreasing or steady, then increasing; or increasing then 
 | |
|               increasing more rapidly; athmospheric pressure now higher
 | |
|               than three hours ago
 | |
|            4: Steady; athmospheric pressure is the same as three hours ago
 | |
|            5: Decreasing, then increasing; athmospheric pressure now is the 
 | |
|               same as or lower than three hours ago
 | |
|            6: 
 | |
|            7:
 | |
|            8:
 | |
|        """
 | |
|        pass
 | |
| 
 | |
|     def getYScale(self, vmin, vmax):
 | |
|         # Y-Achse aufgrund Meßwerten einstellen
 | |
|         diff = vmax - vmin
 | |
|         if diff < 20:
 | |
|             step = 5
 | |
|         elif diff <= 40:
 | |
|             step = 10
 | |
|         else:
 | |
|             step = 15
 | |
|         vmin = int(vmin - (vmin % step))        # Nächstes Vielfaches nach oben
 | |
|         vmax = int(vmax + step - (vmax % step)) # Nächstes Vielfaches nach unten 
 | |
|         return (vmin, vmax, step)
 | |
| 
 | |
|     def draw(self, ctx):
 | |
|         """
 | |
|         Darstellung angelehnt an klassisches Gerät
 | |
|         Daten werden im nichtflüchtigen Speicher gehalten
 | |
|         Da sich die Daten langsam verändern, reicht es, diese z.B. nur alle
 | |
|         30 Sekunden oder langsamer zu laden.
 | |
|         Der aktuelle Wert oben ist natürlich nicht alt.
 | |
| 
 | |
|         Datenreihen 
 | |
|         - 1 Woche, stündlich: 7 * 24 = 168 Meßwerte
 | |
|         - 1 Tag, alle 10 min: 24 * 6 = 144 Meßwerte
 | |
|         Der Druck wird in zwei Bytes abgespeichert. Es wird eine Nachkommastelle
 | |
|         verwendet. Um ohne Fließkommazahlen auszukommen wird der Maßwert einfach
 | |
|         mit 10 multipliziert.
 | |
| 
 | |
|         Darstellung wie Steamrock:
 | |
|         1 Pixel entspricht einem Meßwert alle 15min.
 | |
|         1 Tag hat dementsprechend eine Breite von 48px
 | |
|         """
 | |
| 
 | |
|         timestamp = time.time()
 | |
|         if timestamp - self.refresh >= 30:
 | |
|             self.refresh = timestamp
 | |
|             self.loadData()
 | |
| 
 | |
|         ctx.set_source_rgb(0, 0, 0)
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
| 
 | |
|         # Datenquelle rechts oben
 | |
|         ctx.set_font_size(16)
 | |
|         ctx.move_to(330, 50)
 | |
|         if self.source == 'I':
 | |
|             ctx.show_text("BMP280")
 | |
|         else:
 | |
|             ctx.show_text("N2K Bus")
 | |
|         ctx.stroke()
 | |
|         # Zoomstufe
 | |
|         datastep = self.series[self.zoomindex]
 | |
|         if datastep > 120:
 | |
|             if datastep % 60 == 0:
 | |
|                 fmt = "{:.0f} min"
 | |
|             else:
 | |
|                 fmt = "{:.1f} min"
 | |
|             datastep /= 60
 | |
|         else:
 | |
|             fmt = '{} s'
 | |
|         self.draw_text_center(ctx, 360, 62, fmt.format(datastep))
 | |
| 
 | |
|         # Aktueller Luftdruck hPa
 | |
|         ctx.set_font_size(32)
 | |
|         self.draw_text_center(ctx, 200, 40, self.bd.pressure.format())
 | |
|         #self.draw_text_center(ctx, 200, 40, "1019.2")
 | |
|         ctx.set_font_size(16) 
 | |
|         self.draw_text_center(ctx, 200, 62, "hPa")
 | |
| 
 | |
|         # Trend
 | |
|         ctx.set_font_size(16)
 | |
|         # TODO Trend linie
 | |
|         #trend = 
 | |
|         self.draw_text_center(ctx, 295, 62, "0.0")
 | |
| 
 | |
|         # min/max
 | |
|         ctx.move_to(10, 38)
 | |
|         ctx.show_text(f"min: {self.vmin}")
 | |
|         ctx.move_to(10, 50)
 | |
|         ctx.show_text(f"max: {self.vmax}")
 | |
| 
 | |
|         # Alarm
 | |
|         self.draw_text_center(ctx, 70, 62, "Alarm Off")
 | |
| 
 | |
|         # Hintergrundrahmen
 | |
|         ctx.set_line_width(2)
 | |
| 
 | |
|         ctx.move_to(0, 75)
 | |
|         ctx.line_to(400, 75)
 | |
| 
 | |
|         ctx.move_to(130, 20)
 | |
|         ctx.line_to(130, 75)
 | |
| 
 | |
|         ctx.move_to(270, 20)
 | |
|         ctx.line_to(270, 75)
 | |
| 
 | |
|         ctx.move_to(325, 20)
 | |
|         ctx.line_to(325, 75)
 | |
| 
 | |
|         ctx.stroke()
 | |
| 
 | |
| 
 | |
|         # Diagramm
 | |
|         # --------
 | |
|         ymin = self.scalemin
 | |
|         ymax = self.scalemax
 | |
|         yn = self.scalestep
 | |
|         ystep = (ymax - ymin) / yn
 | |
| 
 | |
|         xstep = 48
 | |
| 
 | |
|         # Ursprung ist rechts unten
 | |
|         x0 = 350
 | |
|         y0 = 270
 | |
|         w = 7 * 48
 | |
|         h = 180
 | |
| 
 | |
|         ctx.set_line_width(1)
 | |
|         ctx.rectangle(x0 - w + 0.5, y0 - h + 0.5, w, h)
 | |
|         ctx.stroke()
 | |
| 
 | |
|         # X-Achse sind Stunden
 | |
|         xn = 0
 | |
|         for xt in [x * -1 * self.zoom[self.zoomindex] for x in range(1,7)]:
 | |
|             xn += 1
 | |
|             ctx.move_to(x0 - xn * xstep + 0.5, y0)
 | |
|             ctx.line_to(x0 - xn * xstep + 0.5, y0 - h)
 | |
|             ctx.stroke()
 | |
|             self.draw_text_center(ctx, x0 - xn * xstep + 0.5, y0 - 8, str(xt), fill=True)
 | |
|         ctx.stroke()
 | |
|         #for x in (1, 2, 3, 4, 5, 6):
 | |
|         #    ctx.move_to(x0 - x * 48 + 0.5, y0 + 0.5)
 | |
|         #    ctx.line_to(x0 - x * 48 + 0.5, y0 - h + 0.5)
 | |
|         #ctx.stroke()
 | |
| 
 | |
|         # Y-Achse
 | |
|         ctx.move_to(x0 + 5.5, y0 + 0.5)
 | |
|         ctx.line_to(x0 + 5.5, y0 - h)
 | |
|         ctx.move_to(x0 - w - 5.5, y0 + 0.5)
 | |
|         ctx.line_to(x0 - w - 5.5, y0 -h )
 | |
|         ctx.stroke()
 | |
| 
 | |
|         dy = 9 # Pixel je hPa
 | |
|         ysmin = self.scalemin
 | |
|         ysmax = self.scalemax
 | |
| 
 | |
|         y = y0 + 0.5
 | |
|         ystep = self.scalestep
 | |
|         ys = ysmin
 | |
|         while y >= y0 - h:
 | |
|             if ys % ystep == 0:
 | |
|                 ctx.move_to(x0 + 10, y + 5.5)
 | |
|                 ctx.show_text(str(ys))
 | |
|                 ctx.move_to(x0 - w - 5, y)
 | |
|                 ctx.line_to(x0 + 5, y)
 | |
|             else:
 | |
|                 ctx.move_to(x0, y)
 | |
|                 ctx.line_to(x0 + 5, y)
 | |
|                 ctx.move_to(x0 - w - 5, y)
 | |
|                 ctx.line_to(x0 - w, y)
 | |
|             y -= dy
 | |
|             ys += 1
 | |
|         ctx.stroke()
 | |
| 
 | |
|         # Meßdaten
 | |
|         if self.data:
 | |
|             for v in self.data:
 | |
|                 x0 -= 1
 | |
|                 if v > 0:
 | |
|                     ctx.rectangle(x0, y0 - (v - ysmin) * dy, 1.5, 1.5)
 | |
|             ctx.fill()
 | |
|         else:
 | |
|             # Keine Daten vorhanden!
 | |
|             # TODO iregndwas nettes anzeigen
 | |
|             pass
 |