Öffnungszeiten auswerten

Begonnen von TomLee, 09 Februar 2025, 19:29:21

Vorheriges Thema - Nächstes Thema

TomLee

Hi,

weil das auslesen der Öffnungszeiten nicht mehr funzt, hab ich :o  angefangen die Logik ;D nachzustellen, wie die Geschäftszeiten bisher bereit gestellt wurden.

Was ich dabei gelernt habe ist, das ich, trotz Hilfe, mit Sicherheit nie Programmierer werde, weil einfach fiel mir das nicht.

Was das "Script" können soll:
  • Ermitteln ob ein Geschäft jetzt offen oder geschlossen ist.
  • Die nächste Öffnungszeit berechnen.
  • Den aktuellen Status in ein Reading schreiben.


package main;

use strict;
use warnings;
use Time::Piece;

sub opentimes_Utils_Initialize {
  my $hash = shift//return;
  return;
}


# Öffnungszeiten für Wochentage
 my %otim = (
    Montag     => "Geschlossen",
    Dienstag   => "11:30-15:00 17:00-23:00",
    Mittwoch   => "11:30-15:00 17:00-23:00",
    Donnerstag => "11:30-15:00 17:00-23:00",
    Freitag    => "11:30-15:00 17:00-23:00",
    Samstag    => "16:30-23:00",
    Sonntag    => "14:00-23:00"
);

sub next_day {
    my $day_offset = shift // 1;
    my $tomorrow = localtime(time + (86400 * $day_offset));
    return $tomorrow->fullday;
}

sub timeAdd($$){
   my ($t,$offset) = @_;
   my @t = split(/:/,$t);
      $t[2] //= 0;
   my $s = $t[0]*3600 + $t[1]*60 + $t[2] + $offset;

   return sprintf("%02d:%02d:%02d", $s/3600, ($s/60)%60, $s%60);
}

# Funktion zur Umwandlung von Uhrzeit in Sekunden seit Mitternacht
sub secondsFromMidnight {
    my ($time_str) = @_;
    my ($hour, $minute, $second) = (0, 0, 0);

    if ($time_str =~ /^(\d{2}):(\d{2}):?(\d{2})?$/) {
        ($hour, $minute, $second) = ($1, $2, defined $3 ? $3 : 0);
    } else {
        die "Ungültiges Zeitformat: $time_str (Erwartet: HH:MM oder HH:MM:SS)\n";
    }

    return ($hour * 3600) + ($minute * 60) + $second;
}

my $current_time = localtime->strftime("%H:%M");
#my $current_time = "23:30";
my $day = localtime->fullday;

# Funktion zur nächsten Öffnungszeit
sub find_next_opening {
    my $self        = shift;
    my $png         = shift // return;  # Default-Return für undefined
    my $ipebuspi    = shift // return;  # Default-Return für undefined



for (my $i = 1; $i <= 7; $i++) {
    $day = next_day($i);  # Gehe Tag für Tag weiter
    my $schedule = $otim{$day} // "Geschlossen";

    next if $schedule eq "Geschlossen";

    if ($schedule =~ /^(\d{2}:\d{2})/) {
        my $ot = $1;
        my $t  = timeAdd($ot, +30);
        readingsSingleUpdate($defs{$self}, "open_times", "Geschlossen\nÖffnet $day um $ot", 1);
        fhem("set $self modifyTimeSpec $t");
        return system("/opt/fhem/www/scripts/epap.sh $png $ipebuspi &");
    }
}

    return "Geschlossen für immer?";  # Falls kein offener Tag gefunden wird
}

# Funktion zur Prüfung, ob aktuell geöffnet ist
sub check_opening_status {
    my $self        = shift;
    my $png         = shift // return;  # Default-Return für undefined
    my $ipebuspi    = shift // return;  # Default-Return für undefined

    my $schedule = $otim{$day};
#Debug $schedule;
   
    return find_next_opening($self, $png, $ipebuspi) if $schedule eq "Geschlossen";

    my $current_seconds = secondsFromMidnight($current_time);
    my @periods = split / /, $schedule;

    foreach my $period (@periods) {
        if ($period =~ /^(\d{2}:\d{2})-(\d{2}:\d{2})$/) {
            my ($start_time, $end_time) = ($1, $2);
            my $start_seconds = secondsFromMidnight($start_time);
            my $end_seconds   = secondsFromMidnight($end_time);

            if ($current_seconds >= $start_seconds && $current_seconds < $end_seconds) {
                readingsSingleUpdate($defs{$self}, "open_times", "Geöffnet\nSchließt um $end_time", 1);
                my $t = timeAdd($end_time, +30);
                fhem("set $self modifyTimeSpec $t");
                return system("/opt/fhem/www/scripts/epap.sh $png $ipebuspi &");
            }

            if ($current_seconds < $start_seconds) {
                readingsSingleUpdate($defs{$self}, "open_times", "Geschlossen\nÖffnet um $start_time", 1);
                my $t = timeAdd($start_time, +30);
                fhem("set $self modifyTimeSpec $t");
                return system("/opt/fhem/www/scripts/epap.sh $png $ipebuspi &");
            }
        }
    }

    # Falls nach dem letzten Zeitraum → nächste Öffnungszeit suchen
    return find_next_opening($self, $png, $ipebuspi);
}
1;


Der Code macht was ich mir vorstelle, mag aber nicht ausschliessen das mir doch etwas entgangen ist, getestet hab ich bis jetzt nicht viel und lange und auch nur mit den Öffnungszeiten in dem Hash.

Wenn sich jemand getriggert fühlt den Code nachzuvollziehen, ich hab nix gegen Verbesserungsvorschläge. Bestimmt hab ich mehrere Dinge umständlich gelöst.

Was mir jetzt am Ende fehlt und ich mich noch nicht wirklich für eine Lösung entscheiden kann, wie und wo ich die Öffnungszeiten für weitere Geschäfte hinterlegen soll.
Ich nutz das mit mehreren at (Geschäfte), dort kann ich mir vorstellen diese in einem userattr festzuhalten und in der myUtils dann in einen Hash zu "übergeben" oder einfach weitere Hashes oben in der myUtils angeben und die dann über Parameterübergabe der Hashreferenz ansprechen? Oder gar nicht mit Hash und die Sache ganz anders angehen?

Gruß Thomas

TomLee

#1
Weil aufgrund der Verwendung der hier so ungeliebten Time::Piece Bibliothek, abhängig von der verwendeten Perl Version, nicht jeder den gezeigten Code ohne anzupassen direkt nachvollziehen kann, hab ich den Code nochmal angepasst und nur unter Verwendung von localtime oder POSIX umgesetzt:

package main;

use strict;
use warnings;
use Time::Piece;

sub mmyUtils_Initialize {
  my $hash = shift//return;
  return;
}


# Öffnungszeiten für Wochentage
 my %otim = (
    Montag     => "Geschlossen",
    Dienstag   => "11:30-15:00 17:00-23:00",
    Mittwoch   => "11:30-15:00 17:00-23:00",
    Donnerstag => "11:30-15:00 17:00-23:00",
    Freitag    => "11:30-15:00 17:00-23:00",
    Samstag    => "16:30-23:00",
    Sonntag    => "14:00-23:00"
);


my $current_time = strftime("%H:%M", localtime);
#my $current_time = "23:30";
my $day = strftime("%A", localtime);
#my $day = "Sonntag";


sub next_day {
    my $day_offset = shift // 1;
    my @tage = qw(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag);   
    my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime(time + (86400 * $day_offset));
    return $tage[$wday]; 
}

sub timeAdd{
   my $t = shift;
   my $offset = shift;
   my @t = split(/:/,$t);
      $t[2] //= 0;
   my $s = $t[0]*3600 + $t[1]*60 + $t[2] + $offset;

   return sprintf("%02d:%02d:%02d", $s/3600, ($s/60)%60, $s%60);
}

# Funktion zur Umwandlung von Uhrzeit in Sekunden seit Mitternacht
sub secondsFromMidnight {
my $time_str = shift;
    my ($hour, $minute, $second) = (0, 0, 0);

    if ($time_str =~ /^(\d{2}):(\d{2}):?(\d{2})?$/) {
        ($hour, $minute, $second) = ($1, $2, defined $3 ? $3 : 0);
    } else {
        die "Ungültiges Zeitformat: $time_str (Erwartet: HH:MM oder HH:MM:SS)\n";
    }

    return ($hour * 3600) + ($minute * 60) + $second;
}

# Funktion zur nächsten Öffnungszeit
sub find_next_opening {
    my $self        = shift;

for (my $i = (localtime)[6]; $i <= 6; $i++) {
    $day = next_day($i);  # Gehe Tag für Tag weiter
    my $schedule = $otim{$day} // "Geschlossen";

    next if $schedule eq "Geschlossen";

    if ($schedule =~ /^(\d{2}:\d{2})/) {
        my $ot = $1;
        my $t  = timeAdd($ot, +30);
        readingsSingleUpdate($defs{$self}, "open_times", "Geschlossen\nÖffnet $day um $ot", 1);
        return fhem("set $self modifyTimeSpec $t");
    }
}

    return "Geschlossen für immer?";  # Falls kein offener Tag gefunden wird
}

# Funktion zur Prüfung, ob aktuell geöffnet ist
sub check_opening_status {
    my $self = shift;

    my $schedule = $otim{$day};
#Debug $schedule;
   
    return find_next_opening($self) if $schedule eq "Geschlossen";

    my $current_seconds = secondsFromMidnight($current_time);
    my @periods = split / /, $schedule;

    foreach my $period (@periods) {
        if ($period =~ /^(\d{2}:\d{2})-(\d{2}:\d{2})$/) {
            my ($start_time, $end_time) = ($1, $2);
            my $start_seconds = secondsFromMidnight($start_time);
            my $end_seconds   = secondsFromMidnight($end_time);

            if ($current_seconds >= $start_seconds && $current_seconds < $end_seconds) {
                readingsSingleUpdate($defs{$self}, "open_times", "Geöffnet\nSchließt um $end_time", 1);
                my $t = timeAdd($end_time, +30);
                return fhem("set $self modifyTimeSpec $t");
            }

            if ($current_seconds < $start_seconds) {
                readingsSingleUpdate($defs{$self}, "open_times", "Geschlossen\nÖffnet um $start_time", 1);
                my $t = timeAdd($start_time, +30);
                return fhem("set $self modifyTimeSpec $t");
            }
        }
    }

    # Falls nach dem letzten Zeitraum → nächste Öffnungszeit suchen
    return find_next_opening($self);
}
1;

defmod at_bla at *15:00:30 {check_opening_status($SELF)}
attr at_bla stateFormat state\
<br>\
open_times

betateilchen

#2
Was möchtest Du denn als Ergebnis bekommen?

Formuliere doch zuerst die konkrete Aufgabe möglichst einfach, damit man Dir helfen kann.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

TomLee

Es soll in ein Reading geschrieben werden ob der Laden zur aktuellen Zeit gerade offen oder geschlossen hat.
Zusätzlich soll ermittelt werden wann er wieder schliesst/öffnet, was auch in dem Reading stehen soll.
Weil das automatisch ablaufen soll, addiere ich eine kurze Zeit x zum ermittelten nächsten Schliess./Öffnungszeit, mit der das at modifiziert und somit wieder die nächste Schliess./Öffnungszeit ermittelt werden kann.

ZitatGeöffnet
Schliesst um 23:00

ZitatGeschlossen
Öffnet um 07:00


Ist morgen geschlossen und es ist nach "Feierabend":
ZitatGeschlossen
Öffnet Dienstag um 11:30

TomLee

ZitatWeil das automatisch ablaufen soll, addiere ich eine kurze Zeit x zum ermittelten nächsten Schliess./Öffnungszeit, mit der das at modifiziert und somit wieder die nächste Schliess./Öffnungszeit ermittelt werden kann.

Jetzt wo ich es ausgesprochen habe, kommt mir, das man sich hier das addieren zu der ermittelten Zeit mit timadd auch sparen kann. Einfach die Zeit x an die ermittelte Zeit anhängen wäre simpler gelöst.

betateilchen

Das geht vermutlich viel einfacher.
Muss ich mir mal ein paar Gedanken machen.

Beispielsweise würde ich nicht mit einem hash arbeiten  8)
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

betateilchen

Warum trägst Du eigentlich nicht die Öffnungszeiten einfach als wiederholende Termine in einen Kalender ein und verwendest das Calendar Modul zur Auswertung? So häufig ändern sich die Öffnungszeiten ja nicht.

Und Deine Frage nach "mehreren Geschäften" wäre damit auch schon automatisch mit gelöst.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

TomLee

Das wäre auch ein denkbarer und spannender Ansatz den ich mir vorstellen kann. Hab mal ein ics.-File mit den Terminen erstellt und eingebunden. Mal schauen was mir dazu einfällt es umzusetzen.

TomLee

Ah, es ist nix anderes am Ende ...

Wenn ich mir weiter dazu Gedanken mache, dann würde mich jetzt interessieren wie Du das siehst mit der Verwendung der Sekunden seit Mitternacht, für den Vergleich ob es vor oder nach der nächsten Öffnungszeit ist? Ist das so OK oder würdest das auch anders angehen?

betateilchen

Zitat von: TomLee am 11 Februar 2025, 18:16:47wie Du das siehst mit der Verwendung der Sekunden seit Mitternacht, für den Vergleich ob es vor oder nach der nächsten Öffnungszeit ist? Ist das so OK oder würdest das auch anders angehen?

Das würde ich auch anders angehen. Es gibt andere Ansätze, den aktuellen Zeitpunkt gegen ein Intervall zu vergleichen.

Zitat von: TomLee am 11 Februar 2025, 18:16:47Ah, es ist nix anderes am Ende ...

Die Variante mit dem Calendar ist durchaus schon ein komplett anderer Ansatz. Die Zeitpunkte kommen quasi "automatisch", weil das Calendar-device sie von sich aus abarbeitet. Da kann man immer zum Start oder zum Ende eines Terminblocks irgendwas beliebiges ausführen - wenn es denn sein muss, auch diverse readings setzen.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

betateilchen

#10
Zitat von: betateilchen am 11 Februar 2025, 16:04:12Das geht vermutlich viel einfacher.
Muss ich mir mal ein paar Gedanken machen.

Nachgedacht, mit dem Ergebnis: Es gibt in FHEM schon einen Lösungsansatz, den man für Dein Anliegen als Basis nehmen kann. Im Attribut disabledForIntervals wird geprüft, oder der aktuelle Zeitpunkt innerhalb angegebener Intervalle liegt.

Darauf aufbauend ist zumindest der Anforderungsteil: "offen bis wann?" oder "geschlossen" recht einfach zu lösen.

  my @zeiten = sort(('0@14:00-0@23:00',
                '2@11:30-2@15:00', '2@17:00-2@23:00',
                '3@11:30-3@15:00', '3@17:00-3@23:00', '3@08:00-3@09:00',
                '4@11:30-4@15:00', '4@17:00-4@23:00',
                '5@11:30-5@15:00', '5@17:00-5@23:00',
                '6@16:30-6@23:00'));

Die Syntax der Zeitangaben im array @zeiten ist in der Doku zum Attribut disabledForIntervals beschrieben. Eine Ausnahme gibt es zur Beschreibung: perl code wird in den Zeitausgaben hier nicht ausgeführt.

Über die Aufgabe, den nächsten Öffnungszeitpunkt zu ermitteln, denke ich als nächstes nach. Das wird etwas aufwändiger, je nachdem, ob das Ergebnis "noch heute" oder "am nächsten Tag" oder "am Sonntag" wird.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

betateilchen

#11
Zitat von: betateilchen am 12 Februar 2025, 08:59:23Über die Aufgabe, den nächsten Öffnungszeitpunkt zu ermitteln, denke ich als nächstes nach. Das wird etwas aufwändiger, je nachdem, ob das Ergebnis "noch heute" oder "am nächsten Tag" oder "am Sonntag" wird.

Here we go...

Es war dann doch einfacher als vermutet.

sub g2 {
  my @zeiten = sort(('0@14:00-0@23:00',
                '2@11:30-2@15:00', '2@17:00-2@23:00',
                '3@11:30-3@15:00', '3@17:00-3@23:00', '3@08:00-3@09:00',
                '4@11:30-4@15:00', '4@17:00-4@23:00',
                '5@11:30-5@15:00', '5@17:00-5@23:00',
                '6@16:30-6@23:00'
));
  my @tage   = ('So','Mo','Di','Mi','Do','Fr','Sa');
 
  my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) = localtime(gettimeofday());
  my $heute = $tage[$wday];
  my $dhms = sprintf("%s\@%02d:%02d:%02d", $wday, $hour, $min, $sec);
 
  foreach my $ft (@zeiten) {
    my ($from, $to) = split("-", $ft);
    if(defined($from) && defined($to)) {
      $from = "$wday\@$from" if(index($from,"@") < 0);
      $to   = "$wday\@$to"   if(index($to,  "@") < 0);
      return "offen bis ".(split('@',$to))[1] if($from le $dhms && $dhms le $to);
    }
  }
  # wenn man hier landet, ist gerade geschlossen...
  for (my $i = 0; $i <= 6; $i++){ # Endlosschleife verhindern
    foreach my $ft (@zeiten) {
      my $day = substr($ft,0,1);
  next if $day < $wday;
  my ($from,$to) = split('-',$ft);
  my $res  = "geschlossen bis ";
     $res .= ($heute ne $tage[$day]) ? "$tage[$day], ":"";
             $res .= (split('@',$from))[1];
      return $res if($from gt $dhms); 
    }
    $wday++;
    $wday = 0 if ($wday == 7);
    $dhms = sprintf("%s\@%02d:%02d:%02d", $wday, $hour, $min, $sec);
  }
  # wenn man hier landet, ist was schiefgegangen
  return "arrghh...";
}
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

betateilchen

Hier noch eine Variante, die das Ergebnis als Werte zurückliefert, mit denen man beliebig weiterarbeiten kann.

sub g3 {
  my @zeiten = sort(('0@14:00-0@23:00',
                '2@11:30-2@15:00', '2@17:00-2@23:00',
                '3@11:30-3@15:00', '3@17:00-3@23:00', '3@08:00-3@09:00',
                '4@11:30-4@15:00', '4@17:00-4@23:00',
                '5@11:30-5@15:00', '5@17:00-5@23:00',
                '6@16:30-6@23:00'
));
  my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) = localtime(gettimeofday());
  my $dhms = sprintf("%s\@%02d:%02d:%02d", $wday, $hour, $min, $sec);
 
  foreach my $ft (@zeiten) {
    my ($from, $to) = split("-", $ft);
    if(defined($from) && defined($to)) {
      $from = "$wday\@$from" if(index($from,"@") < 0);
      $to   = "$wday\@$to"   if(index($to,  "@") < 0);
  return (1,(split('@',$to))) if($from le $dhms && $dhms le $to);
    }
  }
  # wenn man hier landet, ist gerade geschlossen...
  for (my $i = 0; $i <= 6; $i++){ # Endlosschleife verhindern
    foreach my $ft (@zeiten) {
      my $day = substr($ft,0,1);
  next if $day < $wday;
  my ($from,$to) = split('-',$ft);
      return (0,(split('@',$from))) if($from gt $dhms);
    }
    $wday++;
    $wday = 0 if ($wday == 7);
    $dhms = sprintf("%s\@%02d:%02d:%02d", $wday, $hour, $min, $sec);
  }
  # wenn man hier landet, ist was schiefgegangen...
  return ("arrghhh...",undef,undef);
}

Das kann man z.B. so verwenden:

sub g3_test {
  my ($status,$next_day,$next_time) = g3();
  return "Status: $status nTag: $next_day nZeit: $next_time";
}

und bekommt als Ergebnis:

Status: 1 nTag: 3 nZeit: 15:00
Dabei bedeutet:

Status: 1     = es ist gerade (12:20) geöffnet
nTag:   3     = die nächste Änderung ist am Tag 3 (Mittwoch, also heute)
nZeit:  15:00 = um 15:00 Uhr

Da gerade geöffnet ist, ergibt sich daraus logischerweise, dass die nächste Änderung dann eine Schließung ist.
Umgekehrt gilt bei Status = 0, dass gerade geschlossen ist, dann ist die nächste Änderung eine Öffnung.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

TomLee

#13
Danke das Dir über mein Anliegen Gedanken gemacht hast.

Nach einmal schlafen über die Kalender-Variante, hab ich jetzt folgendes userreadings in meiner Definition:

defmod cal_ilmulino Calendar ical file ./bla.ics 86400
attr cal_ilmulino hideOlderThan 0
attr cal_ilmulino room Automationen->Kalender->Test
attr cal_ilmulino userReadings status:triggered {\
my $nastart = fhem('get '.$NAME.' events format:custom="$t1" limit:from=0,count=1',1);;\
my $naend = fhem('get '.$NAME.' events format:custom="$t2" limit:from=0,count=1',1);;\
my $nastartday = strftime("%A",localtime($nastart));;\
if (time >= $nastart && time < $naend) {\
return 'Geöffnet Schliesst um '.strftime("%H:%M",localtime($naend));;}\
if ($nastart-time >= DAYSECONDS) {\
return "Geschlossen Öffnet $nastartday ".strftime("%H:%M",localtime($nastart));;\
}\
if (time < $nastart) {\
return 'Geschlossen Öffnet um '.strftime("%H:%M",localtime($nastart));;\
}}

setstate cal_ilmulino triggered
setstate cal_ilmulino 2025-02-12 12:43:41 calname IlMulino
setstate cal_ilmulino 2025-02-12 12:43:41 lastUpdate 2025-02-12 12:43:39
setstate cal_ilmulino 2025-02-12 12:43:41 nextUpdate 2025-02-13 12:43:39
setstate cal_ilmulino 2025-02-12 12:43:41 nextWakeup 2025-02-13 12:43:39
setstate cal_ilmulino 2025-02-12 12:43:41 state triggered
setstate cal_ilmulino 2025-02-12 12:43:41 status Geschlossen Öffnet Samstag 16:30

Tuts offensichtlich auch, ohne viel Perl-Tamtam, nimmt einem alles das Modul ab.

Das jetzt geschlossen ist liegt daran das ich alle Termine von Mittwoch bis Freitag zum testen gelöscht hab.

Überseh ich was oder ist es wirklich so einfach?

betateilchen

Zitat von: TomLee am 12 Februar 2025, 13:09:56Überseh ich was oder ist es wirklich so einfach?

Es ist so einfach. Deshalb hatte ich Dir ja die Variante mit dem Kalender vorgeschlagen.

Es geht mit einem Calendar-device sogar noch einfacher und völlig ohne userReading.
Aber da kommst Du sicher auch noch drauf  8)
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!