home    Inhaltsverzeichnis

Temperatur- und Helligkeits-Logger V2 - Seite 5

Zeitumstellung

Heute wurde auf Sommerzeit umgestellt .... und offensichtlich ist dabei was schief gegangen.
Im Log finden sich folgende Einträge:
2017.03.26 07:00:05 Logger-03 s=1 - 2017.03.26 02:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 07:00:05 Logger-03 s=1 - 2017.03.26 03:00 - ins= 0, dup=60 (good=60, bad= 0)
2017.03.26 07:00:06 Logger-03 s=1 - 2017.03.26 04:00 - ins=60, dup= 0 (good=60, bad= 0)

2017.03.26 08:00:01 Logger-01 s=1 - 2017.03.26 02:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 08:00:01 Logger-01 s=2 - 2017.03.26 02:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 08:00:01 Logger-01 s=1 - 2017.03.26 03:00 - ins= 0, dup=60 (good=60, bad= 0)
2017.03.26 08:00:01 Logger-01 s=2 - 2017.03.26 03:00 - ins= 0, dup=60 (good=60, bad= 0)
2017.03.26 08:00:01 Logger-01 s=1 - 2017.03.26 04:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 08:00:01 Logger-01 s=2 - 2017.03.26 04:00 - ins=60, dup= 0 (good=60, bad= 0)

2017.03.26 09:38:49 Logger-02 s=1 - 2017.03.26 02:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 09:38:49 Logger-02 s=1 - 2017.03.26 03:00 - ins= 0, dup=60 (good=60, bad= 0)
2017.03.26 09:38:49 Logger-02 s=1 - 2017.03.26 04:00 - ins=60, dup= 0 (good=60, bad= 0)

2017.03.26 11:00:06 Logger-04 s=1 - 2017.03.26 02:00 - ins=60, dup= 0 (good=60, bad= 0)
2017.03.26 11:00:06 Logger-04 s=1 - 2017.03.26 03:00 - ins= 0, dup=60 (good=60, bad= 0)
2017.03.26 11:00:06 Logger-04 s=1 - 2017.03.26 04:00 - ins=60, dup= 0 (good=60, bad= 0)
Die Tabelle Dump hat einen UNIQUE INDEX auf der Spalten-Kombination (location, timestamp).

Im relevanten Zeitbereich befindet sich keine Minute in der Datenbank, zu der nicht auch eine Folge-Minute existiert.
Also dieser Query:
SELECT * FROM Dump WHERE location=5 AND (timestamp+60 NOT IN (SELECT timestamp FROM Dump WHERE location=5))
liefert zwar vier Zeilen, aber die ersten drei Treffer liegen im Februar (dem Installationsdatum der Schaltung) und der vierte Treffer entspricht dem letzten Satz von Sensor-Paar "5".

LoggerView zeigt sowas hier an:
Screenshot von LoggerView bei
        Zeitumstellung

Die Stunde 02:00 fehlt und bei Stunde 04:00 springt die Außentemperatur....was für ein Grauen.
Da wird LoggerView wohl fälschlicherweise irgendwas nach Ortszeit+Sommer/Winterzeit umwandeln.
Der Sprung bei 04:00 ist durch die Unique-Regel in der Datenbank entstanden - also der fehlenden Stunde 03:00-03:59.

Aber wo liegt nun der Fehler....???
Sowohl der DS3231 als auch die Time-Library sollten ausschließlich in Normalzeit laufen.
Zur Sync-Zeit senden die Schaltungen Datum+Zeit als String (Normalzeit).
Der SocketServer wandelt diesen String in einen UNIX-Timestamp um und schreibt ihn in die Datenbank.

Sollte der UNIX-Timestamp etwa in Ortszeit+Sommer/Winterzeit laufen?
Mal testen:
dede@i5:~> python
Python 2.7.12 (default, Jul 01 2016, 15:36:53) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> int(datetime.datetime(2017, 3, 26, 1, 59, 59).strftime("%s"))
1490489999
>>> int(datetime.datetime(2017, 3, 26, 2, 0, 0).strftime("%s"))
1490490000
>>> int(datetime.datetime(2017, 3, 26, 2, 0, 1).strftime("%s"))
1490490001
>>> int(datetime.datetime(2017, 3, 26, 2, 59, 59).strftime("%s"))
1490493599
>>> int(datetime.datetime(2017, 3, 26, 3, 0, 0).strftime("%s"))
1490490000
>>> int(datetime.datetime(2017, 3, 26, 3, 0, 1).strftime("%s"))
1490490001

Na toll.
Von 02:59:59 auf 03:00:00 geht die UNIX-Zeit rückwärts.
Und 2017.03.26/02:00:00 liefert den selben Wert wie 2017.03.26/03:00:00.
Was für ein Trauerspiel. Wer denkt sich denn sowas krankes aus?

Mal sehen, ob und wie ich das irgendwann mal fixen kann.

Wahrscheinlich wäre es schlauer gewesen, wenn ich alles in UTC hätte laufen lassen.
Auch wenn ich auf Python- und C-Seite noch problemlos mit "Ortszeit ohne Sommerzeit" rechnen könnte, bräuchte es auch was performantes auf SQL- bzw. Datenbank-Seite. Also für den View, der den UNIX-Timestamp als Datum+Zeit darstellt.
SQLite3 kennt diesbezüglich jedoch nur die zwei Modifier localtime und utc. Derzeit nutze ich localtime - ohne Modifier wird der Timestamp wohl als UTC betrachtet.


Umstellung auf UTC (oder MEZ-only)

Jetzt will ich erstmal ein paar Definitionen sammeln:
UTC
Weltzeit, aka GMT, aka Zulu-Zeit, keine Umstellung auf Sommer-/Winter-Zeit
MEZ
Mitteleuropäische Zeit, aka CET, Winterzeit, Normalzeit, UTC+1
MESZ
Mitteleuropäische Sommerzeit, aka CEST, aka CET DST, UTC+2
Ortszeit
aka Zonenzeit, die Zeit in einer Zeitzone inkl. Sommer-/Winter-Zeit
Unixzeit
Sekunden seit "01.01.1970 00:00:00"
Schaltsekunde
sporadisch eingefügte Sekunde in die Weltzeit

Bei UTC sollte es keine Zeitsprünge wegen DST-Umstellung geben.
Die Schaltungen melden einen Wert pro Minute - daher sollte die Schaltsekunde hier keine Auswirkungen haben.

Alle meine UNIX-Systeme laufen mit Ortszeit. Also mit UTC+1 oder UTC+2, abhängig von Sommer-/Winter-Zeit.
LoggerView sollte die Zeit-Skala vorzugsweise ebenfalls in Ortszeit anzeigen.
Derzeit laufen die Schaltungen konstant mit MEZ, weil sie die Zeit gemäß folgender Funktion erhalten:
def getNormTime(precise=False):
  [....]
  now=datetime.datetime.now()
  if time.localtime().tm_isdst==1:
    now=now+datetime.timedelta(hours=-1)
  return(now.strftime("%Y.%m.%d %H:%M:%S"))


Für UTC wäre es etwas kürzer:
def getNormTime(precise=False):
  [....]
  return(datetime.datetime.utcnow().strftime("%Y.%m.%d %H:%M:%S"))

Andererseits ist die Zeitzone, mit der die Schaltungen laufen, quasi unwichtig.
Wichtig ist, dass die Zeit lückenlos bzw. ohne Sprünge in der Datenbank landet.
Hätte ich aus Speicherplatz- und Performance-Gründen nicht auf die Speicherung als UNIX-Timestamp umgestellt, wäre jetzt noch alles schön in MEZ - wie es ursprünglich gedacht war.

Die bisher gesammelten Daten ließen sich mit ein (oder zwei) Datenbank-Updates leicht in MEZ-only (oder UTC) umwandeln.
Lediglich die per Unique-Regel verworfene Stunde ist unwiederbringlich weg. Aber was solls....um die Uhrzeit war eh nix los.

Würde ich die Schaltungen zukünftig mit UTC laufen lassen, gäbe es nochmal eine Lücke von einer Stunde.
Daher kann ich sie besser weiter in MEZ laufen lassen und lediglich die Funktion anpassen, die Datum+Zeit vor dem INSERT INTO Dump in einen UNIX-Timestamp wandelt, der in UTC ist.
Und dadurch müsste voraussichtlich nicht einmal die VIEW-Definition für DumpD geändert werden.

dede@i5:~> python
Python 2.7.12 (default, Jul 01 2016, 15:36:53) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> import time
>>> time.tzname
('CET', 'CEST')
>>> time.daylight
1
>>> time.localtime().tm_isdst
1
>>> int(datetime.datetime(1970, 1, 1, 0, 0, 0).strftime("%s"))
-3600
>>> datetime.datetime.fromtimestamp(0)
datetime.datetime(1970, 1, 1, 1, 0)
>>> datetime.datetime.fromtimestamp(1483622580)
datetime.datetime(2017, 1, 5, 14, 23)
>>> datetime.datetime.fromtimestamp(1490723940)
datetime.datetime(2017, 3, 28, 19, 59)
>>>
>>>
>>> int((datetime.datetime(2017, 3, 28, 20, 0)-datetime.datetime(1970, 1, 1)).total_seconds())
1490731200
>>> datetime.datetime.utcfromtimestamp(1490731200)
datetime.datetime(2017, 3, 28, 20, 0)
>>> int((datetime.datetime(2017, 1, 28, 20, 0)-datetime.datetime(1970, 1, 1)).total_seconds())
1485633600
>>> datetime.datetime.utcfromtimestamp(1485633600)
datetime.datetime(2017, 1, 28, 20, 0)
>>>
>>>
>>> int((datetime.datetime(2017, 3, 26, 2, 59, 59)-datetime.datetime(1970, 1, 1)).total_seconds())
1490497199
>>> int((datetime.datetime(2017, 3, 26, 3, 0, 0)-datetime.datetime(1970, 1, 1)).total_seconds())
1490497200
Der Start von UTC ist -1h (60 Sekunden * 60 Minunten = 3600). Die Abweichung wegen MEZ.
Analog andersrum: 0 ergibt 01:00 Uhr am 01.01.1970.
Der Timestamp des ältesten Satzes in der Datenbank liefert die korrekte Zeit: 14:23 Uhr.
Der Timestamp des jüngsten Satzes in der Datenbank liefert 19:59 Uhr. Korrekt wäre 20:59 Uhr.
Dann die von-Hand-Umwandlung von Zeiten, die sonst MESZ und MEZ wären.
Am Schluss noch der Test von oben - nur hier von-Hand in UTC - statt mittels strftime("%s").

Tests gegen SQLite3:
SELECT datetime(0, 'unixepoch'), datetime(0, 'unixepoch', 'localtime')
--> "1970-01-01 00:00:00","1970-01-01 01:00:00"

SELECT datetime(1483622580, 'unixepoch'), datetime(1483622580, 'unixepoch', 'localtime')
--> "2017-01-05 13:23:00","2017-01-05 14:23:00"

SELECT datetime(1490723940, 'unixepoch'), datetime(1490723940, 'unixepoch', 'localtime')
--> "2017-03-28 17:59:00","2017-03-28 19:59:00"

SELECT datetime(1490731200, 'unixepoch'), datetime(1485633600, 'unixepoch')
--> "2017-03-28 20:00:00","2017-01-28 20:00:00"
Der erste Query liefert die Null-Zeit in UTC und in MEZ.
Der zweite Query liefert die Zeit des ältesten Satzes in der Datenbank in UTC und MEZ.
Der dritte Query liefert die Zeit des jüngsten Satzes in der Datenbank in UTC und MESZ.
Der vierte Query ist die Rückrechnung obiger von-Hand-Umwandlung.

Fazit aus den Überlegungen und Tests:
Ist das ätzend...mein Hirn sträubt sich regelrecht gegen diese Art von Denkvorgängen in der Zeit.
Ob wohl ein Time Lord weniger Probleme mit sowas hätte?  ;-)
Hier ein schönes Zitat zum Thema (das ich nur unterstützen kann):
    If you were NOT using UTC, welcome to hell.

Also nochmal....1483622580 ist der älteste Timestamp und entspricht 14:23 Uhr Ortszeit MEZ.
14:23 MEZ entspricht 13:23 UTC.
>>> int((datetime.datetime(2017, 1, 5, 13, 23, 0)-datetime.datetime(1970, 1, 1)).total_seconds())
1483622580
Das hieße also, dass die Sätze vor dem Wechsel auf MESZ nicht verändert werden müssen....???

1490723940 ist der jüngste Timestamp und entspricht 20:59 Uhr Ortszeit MESZ.
Weil er hinter dem Wechsel nach MESZ liegt, wäre in der Datenbank (gemäß obigem Fazit) 3600 addiert worden.
Somit wäre er jetzt 1490723940+3600=1490727540.
20:59 MESZ entspricht 18:59 UTC.
>>> int((datetime.datetime(2017, 3, 28, 18, 59, 0)-datetime.datetime(1970, 1, 1)).total_seconds())
1490727540
>>> datetime.datetime.utcfromtimestamp(1490727540)
datetime.datetime(2017, 3, 28, 18, 59)
>>> datetime.datetime.fromtimestamp(1490727540)
datetime.datetime(2017, 3, 28, 20, 59)


SELECT datetime(1490727540, 'unixepoch'), datetime(1490727540, 'unixepoch', 'localtime')
--> "2017-03-28 18:59:00","2017-03-28 20:59:00"
Passt. Also reicht ein Update für die Sätze, die erfasst wurden, nachdem auf MESZ geschaltet wurde.
Das verwirrt mich jetzt schon etwas.
Aber andererseits ist das auch irgendwie logisch. Schließlich gilt:
    18:59 UTC == 19:59 MEZ == 20:59 MESZ == 1490727540 Unix-Timestamp

Es ist nur eine Frage der Interpretation, ob timestamp als MEZ oder UTC angesehen wird - der Wert ist der selbe.
Das einzige Problem ist, dass der strftime("%s") automatisch von MEZ auf MESZ umgeschaltet hat.
Dadurch wurde "26.03.2017 03:00:00 MEZ" als "26.03.2017 03:00:00 MESZ" interpretiert und falsch nach UNIX-Timestamp gewandelt.

Ab welchem Wert von timestamp muss nun die eine Stunde addiert werden?
Laut Wikipedia wird am letzten Sonntag im März (also z.B. am 26.03.2017) die Zeit von 02:00 MEZ auf 03:00 MESZ vorgestellt.

1490489940
1490490000
1490490060
1490493540
1490493600
1490493660


+60
+60
+58*60
+60
+60
UTC
00:59
01:00
01:01
01:59
02:00
02:01
MEZ
01:59
{02:00}
{02:01}
{02:59}
03:00
03:01
MESZ
02:59
03:00 03:01
03:59
04:00
04:01
Die Zeit 02:00:00 bis 02:59:59 gibt es am 26.03.2017 eigentlich nicht.
Der strftime("%s") läuft offensichtlich bis 02:59:59 in MEZ, ab 03:00:00 läuft er in MESZ.
Somit sollten die Werte ab 03:00 MEZ mit dem Timestamp von 03:00 MESZ (also 02:00 MEZ) in der Datenbank abgelegt worden sein. Wegen der Unique-Regel wurden sie abgewiesen und erst wieder ab 04:00 MESZ angenommen.
Folglich muss ab 1490493600 bzw. 02:00 UTC eine Stunde auf timestamp addiert werden.
Das ergäbe eine Daten-Lücke von 03:00 MEZ bis 03:59 MEZ (bzw. 04:00 MESZ bis 04:59 MESZ) und würde damit auch zu dem Sprung der Außentemperatur in obigem Screenshot von LoggerView passen.

Allerdings wird das wohl nicht mit einem einfachen Update-Statement zu machen sein. Also so:
UPDATE Dump SET timestamp=timestamp+3600 WHERE timestamp>=1490493600
...wird es eher nicht funktionieren. Und zwar wegen der Unique-Regel.
Die Änderungs-Reihenfolge muss unbedingt von jung nach alt gehen, weil es ansonsten den +3600-Wert immer schon gibt (außer in der jüngsten Stunde).
Aber statt dafür jetzt extra ein Progrämmchen zu bauen, kann ich es auch so machen, wie hier vorgeschlagen.
Also die relevanten Sätze in einem ersten Schritt auf -(timestamp+3600) setzen und in einem zweiten Schritt dann final auf -timestamp.
UPDATE Dump SET timestamp=-(timestamp+3600) WHERE timestamp>=1490493600
UPDATE Dump SET timestamp=-timestamp WHERE timestamp<1

Bevor ich dem SocketServer die geänderte Datenbank unterschiebe, muss der so geändert worden sein, dass er Datum+Zeit nicht mehr mittels:
d=datetime.datetime(year, month, day, hour, minute, 0)
int(d.strftime("%s"))
sondern von-Hand mittels:
d=int((datetime.datetime(year, month, day, hour, minute)-datetime.datetime(1970, 1, 1)).total_seconds())-3600
in einen Timestamp umwandelt.
Die -3600 dient dazu, um die in MEZ gelieferte Zeit nach UTC zu wandeln.
Kurz ein Test:
>>> d=int((datetime.datetime(2017, 3, 26, 3, 0)-datetime.datetime(1970, 1, 1)).total_seconds())-3600
>>> d
1490493600
1490493600 passt zu obiger Tabelle für 03:00 MEZ.

Nun muss ich nur noch LoggerView daraufhin prüfen (und ggf. anpassen), ob die Ortszeit nur für unwichtige Dinge wie etwa die Darstellung der Zeit-Skala genutzt wird.
Die Berechnung einer Dauer bzw. eines Abstands sollte besser mit UTC laufen. Wegen sowas hier:
>>> d1=datetime.datetime(2017, 3, 26, 4, 0, 0)
>>> d2=datetime.datetime(2017, 3, 25, 4, 0, 0)
>>>
>>> int((d1-d2).total_seconds())
86400
>>>
>>> int(d1.strftime("%s"))-int(d2.strftime("%s"))
82800
>>>
>>> d0=datetime.datetime(1970, 1, 1)
>>> int((d1-d0).total_seconds())-int((d2-d0).total_seconds())
86400
>>> 60*60*24
86400

Hier die korrigierte Version vom SocketServer und von LoggerView.