diff --git a/FHEM/98_JsonMod.pm b/FHEM/98_JsonMod.pm
new file mode 100644
index 000000000..032099538
--- /dev/null
+++ b/FHEM/98_JsonMod.pm
@@ -0,0 +1,1633 @@
+# $Id$
+###############################################################################
+#
+# This file is part of fhem.
+#
+# Fhem is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Fhem is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with fhem. If not, see .
+#
+#
+###############################################################################
+
+package main;
+
+use 5.018;
+use feature qw( lexical_subs );
+
+use strict;
+use warnings;
+use utf8;
+use Time::Local qw( timelocal timegm );
+use Text::Balanced qw ( extract_codeblock extract_delimited );
+use HttpUtils;
+
+#use Memory::Usage;
+
+no warnings qw( experimental::lexical_subs );
+
+sub JsonMod_Initialize {
+ my ($hash) = @_;
+
+ my @attrList;
+ {
+ no warnings qw( qw );
+ @attrList = qw(
+ httpHeader:textField-long
+ httpTimeout
+ readingList:textField-long
+ disable:0,1
+ interval
+ );
+ };
+
+ $hash->{'DefFn'} = 'JsonMod_Define';
+ $hash->{'UndefFn'} = 'JsonMod_Undef';
+ $hash->{'DeleteFn'} = 'JsonMod_Delete';
+ $hash->{'SetFn'} = 'JsonMod_Set';
+ $hash->{'AttrFn'} = 'JsonMod_Attr';
+ $hash->{'NotifyFn'} = 'JsonMod_Notify';
+ $hash->{'NOTIFYDEV'} = 'TYPE=Global';
+ #$hash->{'NotifyOrderPrefix'} = "50-";
+ $hash->{'AttrList'} = join(' ', @attrList)." $readingFnAttributes ";
+
+ return undef;
+};
+
+sub JsonMod_Define {
+ my ($hash, $def) = @_;
+ my ($name, $type, $source) = split /\s/, $def, 3;
+
+ my $cvsid = '$Id$';
+ $cvsid =~ s/^.*pm\s//;
+ $cvsid =~ s/Z\s\S+\s\$$//;
+
+ $hash->{'CONFIG'}->{'IN_REQUEST'} = 0;
+ $hash->{'CRON'} = JsonMod::Cron->new();
+
+ return "no FUUID, is fhem up to date?" if (not $hash->{'FUUID'});
+ return "wrong source definition" if ($source !~ m/^(https:|http:|file:)/);
+ $hash->{'CONFIG'}->{'SOURCE'} = $source;
+
+ InternalTimer(0, \&JsonMod_Run, $hash) if ($init_done);
+ return;
+};
+
+# reread / temporary remove
+sub JsonMod_Undef {
+ my ($hash, $name) = @_;
+ RemoveInternalTimer($hash, \&JsonMod_DoTimer);
+ return;
+};
+
+# delete / permanently remove
+sub JsonMod_Delete {
+ my ($hash, $name) = @_;
+ my $error;
+ # remove secret
+ setKeyValue($hash->{'FUUID'}, undef);
+ return $error;
+};
+
+sub JsonMod_Run {
+ my ($hash) = @_;
+ my $name = $hash->{'NAME'};
+
+ JsonMod_ReadPvtConfig($hash);
+ return if IsDisabled($name);
+
+ my $cron = AttrVal($name, 'interval', '0 * * * *');
+ $hash->{'CONFIG'}->{'CRON'} = \$cron;
+ JsonMod_StartTimer($hash);
+ JsonMod_ApiRequest($hash);
+ return;
+};
+
+sub JsonMod_Set {
+ my ($hash, $name, $cmd, @args) = @_;
+
+ return "Unknown argument $cmd, choose one of secret" if ($cmd eq '?');
+
+ if ($cmd eq 'secret') {
+ if (not $args[1] and (exists($hash->{'CONFIG'}->{'SECRET'}->{$args[0]}))) {
+ delete $hash->{'CONFIG'}->{'SECRET'}->{$args[0]};
+ JsonMod_WritePvtConfig($hash);
+ } elsif ($args[1]) {
+ $hash->{'CONFIG'}->{'SECRET'}->{$args[0]} = \$args[1];
+ JsonMod_WritePvtConfig($hash);
+ };
+ return;
+ };
+
+ if ($cmd eq 'test') {
+ my $filename = './log/goessner.json';
+ my $data;
+ open(my $fh, '<', $filename) or return "cannot open file $filename";
+ {
+ local $/;
+ $data = <$fh>;
+ }
+ close($fh);
+ my @test = qw (
+ $..*
+ $.store.book[*].author
+ $..author
+ $.store..price
+ $..book[2]
+ );
+ my $json = JsonMod::JSON::StreamReader->new()->parse($data);
+ my $path = JsonMod::JSON::Path->new($json);
+ foreach my $q (@test) {
+ print "****************************************************\n";
+ my $query = $path->get($q);
+ $query->getResultNormVal();
+ print "****************************************************\n";
+ };
+ my $query = $path->get('$.store.book[?(@.price < 10)]');
+ $query->getResultNormVal();
+ };
+
+ return;
+};
+
+sub JsonMod_Attr {
+ my ($cmd, $name, $attrName, $attrValue) = @_;
+ my $hash = $defs{$name};
+ $attrValue //= '';
+ #my $result;
+
+ if ($cmd eq 'set') {
+ if ($attrName eq 'disable') {
+ if ($attrValue) {
+ JsonMod_StopTimer($hash);
+ } else {
+ JsonMod_StopTimer($hash);
+ JsonMod_StartTimer($hash); # unless IsDisabled($name);
+ };
+ };
+ if ($attrName eq 'interval') {
+ if (split(/ /, $attrValue) == 5) {
+ if ($hash->{'CRON'}->validate($attrValue)) {
+ $hash->{'CONFIG'}->{'CRON'} = \$attrValue;
+ return if (!$init_done);
+ JsonMod_StopTimer($hash);
+ JsonMod_StartTimer($hash) unless IsDisabled($name);
+ return;
+ } else {
+ return "wrong interval expression (cron)"
+ };
+ };
+ return "wrong interval expression";
+ };
+ };
+ if ($cmd eq 'delete') {
+ if ($attrName eq 'interval') {
+ $hash->{'CONFIG'}->{'CRON'} = \'0 * * * *';
+ JsonMod_StopTimer($hash);
+ JsonMod_StartTimer($hash); # unless IsDisabled($name);
+ };
+ };
+};
+
+sub JsonMod_Notify {
+ my ($hash, $dev) = @_;
+ my $name = $hash->{'NAME'};
+ return undef if(IsDisabled($name));
+
+ my $events = deviceEvents($dev, 1);
+ return if(!$events);
+
+ foreach my $event (@{$events}) {
+ my @e = split /\s/, $event;
+ JsonMod_Logger($hash, 5, 'event:[%s], device:[%s]', $event, $dev->{'NAME'});
+ if ($dev->{'TYPE'} eq 'Global') {
+ if ($e[0] and $e[0] eq 'INITIALIZED') {
+ JsonMod_Run($hash);
+ };
+ };
+ };
+ return;
+};
+
+# retrieve secrets
+sub JsonMod_ReadPvtConfig {
+ my ($hash) = @_;
+
+ my sub clean {
+ $hash->{'CONFIG'}->{'SECRET'} = {};
+ return;
+ };
+
+ my ($error, $data) = getKeyValue($hash->{'FUUID'});
+ if ($error or not $data) {
+ return clean();
+ } else {
+ $data = MIME::Base64::decode($data);
+ $data = JsonMod::JSON::StreamReader->new()->parse($data) or do {return clean()};
+ return clean() if (ref($data) ne 'HASH');
+ };
+
+ foreach my $k (keys %{$data->{'SECRET'}}) {
+ $hash->{'CONFIG'}->{'SECRET'}->{$k} = \$data->{'SECRET'}->{$k};
+ };
+ $hash->{'SECRETS'} = join ", ", keys (%{$hash->{'CONFIG'}->{'SECRET'}});
+ return;
+};
+
+# store secrets
+sub JsonMod_WritePvtConfig {
+ my ($hash) = @_;
+
+ my $data;
+ foreach my $k (keys (%{$hash->{'CONFIG'}->{'SECRET'}})) {
+ $data->{'SECRET'}->{$k} = ${$hash->{'CONFIG'}->{'SECRET'}->{$k}};
+ };
+ $hash->{'SECRETS'} = join ", ", keys (%{$hash->{'CONFIG'}->{'SECRET'}});
+ my $key = $hash->{'FUUID'};
+ my $val = MIME::Base64::encode(JsonMod::JSON::StreamWriter->new()->parse($data));
+ my $error = setKeyValue($key, $val);
+ return;
+};
+
+sub JsonMod_DoReadings {
+ my ($hash, $data) = @_;
+ my $name = $hash->{'NAME'};
+
+ my $path = JsonMod::JSON::Path->new($data);
+
+ my $newReadings = {};
+ my $oldReadings = {};
+ foreach my $key (keys %{$hash->{'READINGS'}}) {
+ $oldReadings->{$key} = 0;
+ };
+
+ my sub jsonPathf {
+ no warnings qw( redundant missing );
+ my ($jsonPathExpression, $format) = @_;
+ $format //= '%s';
+ my $value = $path->get($jsonPathExpression)->getResultValue();
+ #$path->get($jsonPathExpression)->getResultNormVal();
+ $value = $value->[0] if (ref($value) eq 'ARRAY' and scalar(@{$value}) > 0);
+ if (defined($value)) {
+ return sprintf($format, $value);
+ } else {
+ return undef;
+ };
+ };
+
+ my sub jsonPath {
+ my ($jsonPathExpression) = @_;
+ return $path->get($jsonPathExpression)->getResultValue();
+ };
+
+ # depraced
+ my sub singleReading {
+ my ($reading, $value, $default) = @_;
+ $value = $value->[0] if (ref($value) eq 'ARRAY' and scalar(@{$value}) > 0);
+ $value //= $default;
+ $newReadings->{$reading} = $value;
+ $oldReadings->{$reading} = 1;
+ return;
+ };
+
+ my sub concat {
+ my @args = @_;
+ return sub {
+ my ($o) = @_;
+ my $result = '';
+ foreach my $arg (@args) {
+ if (ref($arg) eq 'CODE') {
+ $result .= $arg->($o);
+ } elsif (ref($arg) eq '') {
+ $result .= $arg;
+ } else {
+ die('syntax');
+ };
+ };
+ return $result;
+ };
+ };
+
+ my sub propertyf {
+ my ($p, $default, $format) = @_;
+ $default //= '';
+ $format //= '';
+ return sub {
+ my ($o) = @_;
+ if (ref($o) eq 'CODE') {
+ return $o->($p, $default);
+ } elsif (ref($o) eq 'HASH') {
+ my $result = $o->{$p} if (exists($o->{$p}));
+ if (defined($result)) {
+ if (ref($result) eq '') {
+ return sprintf ($format, $result);
+ } else {
+ return $result;
+ };
+ } else {
+ return $default;
+ };
+ } elsif (ref($o) eq 'ARRAY') {
+ my $result = $o->[$p] if ((scalar @{$o}) > ($p + 0));
+ if (defined($result)) {
+ if (ref($result) eq '') {
+ return sprintf ($format, $result);
+ } else {
+ return $result;
+ };
+ } else {
+ return $default;
+ };
+ } elsif (ref($o) eq '') {
+ return $o;
+ } else {
+ die('syntax');
+ };
+ };
+ };
+
+ my sub property {
+ my ($p, $default) = @_;
+ $default //= '';
+ return sub {
+ my ($o) = @_;
+ if (ref($o) eq 'CODE') {
+ return $o->($p, $default);
+ } elsif (ref($o) eq 'HASH') {
+ return $o->{$p} if (exists($o->{$p}));
+ return $default;
+ } elsif (ref($o) eq 'ARRAY') {
+ return $o->[$p] if ((scalar @{$o}) > ($p + 0));
+ return $default;
+ } elsif (ref($o) eq '') {
+ return $o;
+ } else {
+ die('syntax');
+ };
+ };
+ };
+
+ my $_index = 0;
+ my sub index {
+ #my $index = 0;
+ return sub {
+ return $_index;
+ };
+ };
+
+ my sub multi {
+ my ($value, @refs) = @_;
+ die ('jsonPath result not a list') if (ref($value) ne 'ARRAY');
+
+ $_index = 0;
+ foreach my $element (@{$value}) {
+ #use Data::Dumper;
+ #print Dumper $element;
+ my @reading;
+ foreach my $ref (@refs) {
+ push @reading, $ref->($element);
+ };
+ $_index++;
+ $newReadings->{$reading[0]} = $reading[1];
+ $oldReadings->{$reading[0]} = 1;
+ };
+ };
+
+ # value (mostly jsonPath) / reading name / default if value is not available
+ my sub single {
+ my ($value, $reading, $default) = @_;
+ $value = $value->[0] if (ref($value) eq 'ARRAY' and scalar(@{$value}) > 0);
+ #use Data::Dumper;
+ #print $value;
+ $value //= $default;
+ $newReadings->{$reading} = $value;
+ $oldReadings->{$reading} = 1;
+ return;
+ };
+
+ if (my $readingList = AttrVal($name, 'readingList', '')) {
+ my $NAME = $name;
+ if (not eval $readingList and $@) {
+ JsonMod_Logger($hash, 2, 'error while evaluating readingList: %s', $@);
+ return;
+ };
+ if (keys %{$newReadings}) {
+ readingsBeginUpdate($hash);
+ foreach my $k (keys %{$newReadings}) {
+ readingsBulkUpdate($hash, $k, $newReadings->{$k});
+ };
+ # not used anymore
+ foreach my $k (keys %{$oldReadings}) {
+ readingsDelete($hash, $k) if ($oldReadings->{$k} == 0);
+ };
+ readingsEndUpdate($hash, 1);
+ };
+ };
+};
+
+sub JsonMod_StartTimer {
+ my ($hash) = @_;
+ my $name = $hash->{'NAME'};
+
+ my $cron = ${$hash->{'CONFIG'}->{'CRON'}};
+ my @t = localtime(Time::HiRes::time());
+ $t[4] += 1;
+ $t[5] += 1900;
+ my @r = $hash->{'CRON'}->next($cron, @t);
+ my $ts = timelocal(0, $r[0], $r[1], $r[2], $r[3] -1, $r[4] -1900);
+ $hash->{'NEXT'} = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $r[4], $r[3], $r[2], $r[1], $r[0], 0);
+ JsonMod_Logger($hash, 4, 'next request: %04d.%02d.%02d %02d:%02d:%02d', $r[4], $r[3], $r[2], $r[1], $r[0], 0);
+ InternalTimer($ts, \&JsonMod_DoTimer, $hash);
+ return;
+};
+
+sub JsonMod_StopTimer {
+ my ($hash) = @_;
+ $hash->{'NEXT'} = 'NEVER';
+ RemoveInternalTimer($hash, \&JsonMod_DoTimer);
+ return;
+};
+
+sub JsonMod_DoTimer {
+ my ($hash) = @_;
+ JsonMod_Logger($hash, 4, 'start request');
+ JsonMod_StartTimer($hash);
+ # request in flight ? cancel
+ return if ($hash->{'CONFIG'}->{'IN_REQUEST'});
+ JsonMod_ApiRequest($hash);
+ return;
+};
+
+sub JsonMod_ApiRequest {
+ my ($hash) = @_;
+ my $name = $hash->{'NAME'};
+
+ # prevent simultaneous request
+ return if ($hash->{'CONFIG'}->{'IN_REQUEST'});
+ $hash->{'CONFIG'}->{'IN_REQUEST'} = 1;
+
+ my $param = {
+ 'hash' => $hash,
+ 'cron' => $hash->{'CONFIG'}->{'CRON'},
+ 'callback' => \&JsonMod_ApiResponse
+ };
+
+ my $source = $hash->{'CONFIG'}->{'SOURCE'};
+ # fill in SECRET if available
+ $source =~ s/(\[.+?\])/(exists($hash->{'CONFIG'}->{'SECRET'}->{substr($1,1,length($1)-2)}))?${$hash->{'CONFIG'}->{'SECRET'}->{substr($1,1,length($1)-2)}}:$1/eg and
+ $param->{'hideurl'} = 1;
+ $param->{'url'} = $source;
+
+ my $header = AttrVal($name, 'httpHeader', '');
+ if ($header) {
+ $header =~ s/(\[.+?\])/(exists($hash->{'CONFIG'}->{'SECRET'}->{substr($1,1,length($1)-2)}))?${$hash->{'CONFIG'}->{'SECRET'}->{substr($1,1,length($1)-2)}}:$1/eg;
+ };
+ $header .= "\r\nAccept: application/json" unless ($header =~ m'Accept: application/json');
+ $param->{'header'} = $header;
+ $param->{'loglevel'} = AttrVal($name, 'verbose', 3);
+ $param->{'timeout'} = AttrVal($name, 'httpTimeout', 30);
+ HttpUtils_NonblockingGet($param);
+ return;
+};
+
+sub JsonMod_ApiResponse {
+ my ($param, $err, $data) = @_;
+ my $hash = $param->{'hash'};
+
+ # cron settings changed while doing request. discard silently
+ return if ($param->{'cron'} ne $hash->{'CONFIG'}->{'CRON'});
+ # check for error
+ # TODO
+ $hash->{'CONFIG'}->{'IN_REQUEST'} = 0;
+
+ $hash->{'API_LAST_RES'} = Time::HiRes::time();
+ $hash->{'SOURCE'} = sprintf('%s (%s)', $param->{'url'} //= '', $param->{'code'} //= '');
+
+ my sub doError {
+ my ($msg) = @_;
+ $hash->{'API__LAST_MSG'} = $msg;
+ my $next = Time::HiRes::time() + 600;
+ $hash->{'API__NEXT_REQ'} = $next;
+ return InternalTimer($next, \&JsonMod_ApiRequest, $hash);
+ };
+
+ if ($err) {
+ JsonMod_Logger($hash, 2, 'http request error: %s', $err);
+ return doError($err);
+ };
+
+ my $rs = JsonMod::JSON::StreamReader->new()->parse($data);
+ if (not $rs or ((ref($rs) ne 'HASH') and ref($rs) ne 'ARRAY')) {
+ return doError('invalid server response');
+ };
+
+ #my $mu = Memory::Usage->new();
+ #$mu->record('before');
+ JsonMod_DoReadings($hash, $rs);
+ #$mu->record('after');
+ #$mu->dump();
+
+ return;
+};
+
+sub JsonMod_Logger {
+ my ($hash, $verbose, $message, @args) = @_;
+ my $name = $hash->{'NAME'};
+ no warnings qw( redundant missing );
+ Log3 ($name, $verbose, sprintf('[%s] '.$message, $name, @args));
+ return;
+};
+
+
+###############################################################################
+# credits to David Oswald
+# http://cpansearch.perl.org/src/DAVIDO/JSON-Tiny-0.58/lib/JSON/Tiny.pm
+package JsonMod::JSON::StreamWriter;
+
+use strict;
+use warnings;
+use utf8;
+use B;
+
+my ($escape, $reverse);
+
+BEGIN {
+ eval "use JSON::XS;1;" or do {
+ if (not $main::_JSON_PP_WARN) {
+ main::Log3 (undef, 3, sprintf('json [%s] is PP. Consider installing JSON::XS', __PACKAGE__));
+ $main::_JSON_PP_WARN = 1;
+ };
+ };
+};
+
+BEGIN {
+ %{$escape} = (
+ '"' => '"',
+ '\\' => '\\',
+ '/' => '/',
+ 'b' => "\x08",
+ 'f' => "\x0c",
+ 'n' => "\x0a",
+ 'r' => "\x0d",
+ 't' => "\x09",
+ 'u2028' => "\x{2028}",
+ 'u2029' => "\x{2029}"
+ );
+ %{$reverse} = map { $escape->{$_} => "\\$_" } keys %{$escape};
+ for(0x00 .. 0x1f) {
+ my $packed = pack 'C', $_;
+ $reverse->{$packed} = sprintf '\u%.4X', $_ unless defined $reverse->{$packed};
+ };
+};
+
+sub new {
+ my $class = shift;
+ my $self = {};
+ bless $self, $class;
+ return $self;
+};
+
+sub parse {
+ my ($self, $data) = @_;
+ my $stream;
+
+ # use JSON::XS if available
+ my $xs = eval 'JSON::XS::encoode_json($data)';
+ return $xs if ($xs);
+
+ if (my $ref = ref $data) {
+ use Encode;
+ return Encode::encode_utf8($self->addValue($data));
+ };
+};
+
+sub addValue {
+ my ($self, $data) = @_;
+ if (my $ref = ref $data) {
+ return $self->addONode($data) if ($ref eq 'HASH');
+ return $self->addANode($data) if ($ref eq 'ARRAY');
+ };
+ return 'null' unless defined $data;
+ return $data
+ if B::svref_2object(\$data)->FLAGS & (B::SVp_IOK | B::SVp_NOK)
+ # filter out "upgraded" strings whose numeric form doesn't strictly match
+ && 0 + $data eq $data
+ # filter out inf and nan
+ && $data * 0 == 0;
+ # String
+ return $self->addString($data);
+};
+
+sub addString {
+ my ($self, $str) = @_;
+ $str =~ s!([\x00-\x1f\x{2028}\x{2029}\\"/])!$reverse->{$1}!gs;
+ return "\"$str\"";
+};
+
+sub addONode {
+ my ($self, $object) = @_;
+ my @pairs = map { $self->addString($_) . ':' . $self->addValue($object->{$_}) }
+ sort keys %$object;
+ return '{' . join(',', @pairs) . '}';
+};
+
+sub addANode {
+ my ($self, $array) = @_;
+ return '[' . join(',', map { $self->addValue($_) } @{$array}) . ']';
+};
+
+# static, sanitize a json message
+
+###############################################################################
+# credits to David Oswald
+# http://cpansearch.perl.org/src/DAVIDO/JSON-Tiny-0.58/lib/JSON/Tiny.pm
+package JsonMod::JSON::StreamReader;
+
+use strict;
+use warnings;
+use utf8;
+
+BEGIN {
+ eval "use JSON::XS;1;" or do {
+ if (not $main::_JSON_PP_WARN) {
+ main::Log3 (undef, 3, sprintf('json [%s] is PP. Consider installing JSON::XS', __PACKAGE__));
+ $main::_JSON_PP_WARN = 1;
+ };
+ };
+};
+
+
+sub new {
+ my $class = shift;
+ my $self = {};
+ bless $self, $class;
+ return $self;
+};
+
+sub parse {
+ my ($self, $in) = @_;
+ my $TRUE = 1;
+ my $FALSE = 0;
+
+ local *exception = sub {
+ my ($e) = @_;
+ # Leading whitespace
+ m/\G[\x20\x09\x0a\x0d]*/gc;
+ # Context
+ my $context = 'Malformed JSON: ' . shift;
+ if (m/\G\z/gc) {
+ $context .= ' before end of data';
+ } else {
+ my @lines = split "\n", substr($_, 0, pos);
+ $context .= ' at line ' . @lines . ', offset ' . length(pop @lines || '');
+ };
+ die "$context";
+ };
+
+ local *_decode_string = sub {
+ my $pos = pos;
+
+ # Extract string with escaped characters
+ m!\G((?:(?:[^\x00-\x1f\\"]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc; # segfault on 5.8.x in t/20-mojo-json.t
+ my $str = $1;
+
+ # Invalid character
+ unless (m/\G"/gc) { #"
+ exception('Unexpected character or invalid escape while parsing string')
+ if m/\G[\x00-\x1f\\]/;
+ exception('Unterminated string');
+ };
+
+ # Unescape popular characters
+ if (index($str, '\\u') < 0) {
+ #no warnings;
+ $str =~ s!\\(["\\/bfnrt])!$self->{'ESCAPE'}->{$1}!gs;
+ return $str;
+ };
+
+ # Unescape everything else
+ my $buffer = '';
+ while ($str =~ m/\G([^\\]*)\\(?:([^u])|u(.{4}))/gc) {
+ $buffer .= $1;
+ # Popular character
+ if ($2) {
+ $buffer .= $self->{'ESCAPE'}->{$2};
+ } else { # Escaped
+ my $ord = hex $3;
+ # Surrogate pair
+ if (($ord & 0xf800) == 0xd800) {
+ # High surrogate
+ ($ord & 0xfc00) == 0xd800
+ or pos($_) = $pos + pos($str), exception('Missing high-surrogate');
+ # Low surrogate
+ $str =~ m/\G\\u([Dd][C-Fc-f]..)/gc
+ or pos($_) = $pos + pos($str), exception('Missing low-surrogate');
+ $ord = 0x10000 + ($ord - 0xd800) * 0x400 + (hex($1) - 0xdc00);
+ };
+ # Character
+ $buffer .= pack 'U', $ord;
+ };
+ };
+ # The rest
+ return $buffer . substr $str, pos $str, length $str;
+ };
+
+ local *_decode_object = sub {
+ my %hash;
+ until (m/\G[\x20\x09\x0a\x0d]*\}/gc) {
+ # Quote
+ m/\G[\x20\x09\x0a\x0d]*"/gc
+ or exception('Expected string while parsing object');
+ # Key
+ my $key = _decode_string();
+ # Colon
+ m/\G[\x20\x09\x0a\x0d]*:/gc
+ or exception('Expected colon while parsing object');
+ # Value
+ $hash{$key} = _decode_value();
+ # Separator
+ redo if m/\G[\x20\x09\x0a\x0d]*,/gc;
+ # End
+ last if m/\G[\x20\x09\x0a\x0d]*\}/gc;
+ # Invalid character
+ exception('Expected comma or right curly bracket while parsing object');
+ };
+ return \%hash;
+ };
+
+ local *_decode_array = sub {
+ my @array;
+ until (m/\G[\x20\x09\x0a\x0d]*\]/gc) {
+ # Value
+ push @array, _decode_value();
+ # Separator
+ redo if m/\G[\x20\x09\x0a\x0d]*,/gc;
+ # End
+ last if m/\G[\x20\x09\x0a\x0d]*\]/gc;
+ # Invalid character
+ exception('Expected comma or right square bracket while parsing array');
+ };
+ return \@array;
+ };
+
+ local *_decode_value = sub {
+ # Leading whitespace
+ m/\G[\x20\x09\x0a\x0d]*/gc;
+ # String
+ return _decode_string() if m/\G"/gc;
+ # Object
+ return _decode_object() if m/\G\{/gc;
+ # Array
+ return _decode_array() if m/\G\[/gc;
+ # Number
+ # jh: failed with 0123
+ #my ($i) = /\G([-]?(?:0(?!\d)|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc;
+ my ($i) = /\G(?=.)([+-]?([0-9]*)(\.([0-9]+))?)([eE][+-]?\d+)?/gc;
+ return 0 + $i if defined $i;
+ # True
+ { no warnings;
+ return $TRUE if m/\Gtrue/gc;
+ # False
+ return $FALSE if m/\Gfalse/gc;
+ };
+ # Null
+ return undef if m/\Gnull/gc; ## no critic (return)
+ # Invalid character
+ exception('Expected string, array, object, number, boolean or null');
+ };
+
+ local *_decode = sub {
+ my $valueref = shift;
+ eval {
+ # Missing input
+ die "Missing or empty input\n" unless length( local $_ = shift );
+ # UTF-8
+ $_ = eval { Encode::decode('UTF-8', $_, 1) } unless shift;
+ die "Input is not UTF-8 encoded\n" unless defined $_;
+ # Value
+ $$valueref = _decode_value();
+ # Leftover data
+ return m/\G[\x20\x09\x0a\x0d]*\z/gc || exception('Unexpected data');
+ } ? return undef : chomp $@;
+ return $@;
+ };
+
+ # use JSON::XS if available
+ my $xs = eval 'JSON::XS::decode_json($in)';
+ return $xs if ($xs);
+
+ my $err = _decode(\my $value, $in, 1);
+ return defined $err ? $err : $value;
+};
+
+# https://github.com/json-path/JsonPath
+# https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html#examples
+
+package JsonMod::JSON::Path;
+
+use strict;
+use warnings;
+use utf8;
+
+sub new {
+ my ($class, $o) = @_;
+ my $self = bless {}, $class;
+ $self->{'root'} = JsonMod::JSON::Path::Node->new($o);
+ return $self;
+};
+
+# valid:
+# $..
+# $.
+# $[property]
+# property
+# invalid ubt accepted:
+# ..property
+sub get {
+ my ($self, $path) = @_;
+ my $query = JsonMod::JSON::Path::Query->new();
+ #print "get $path\n";
+ $path =~ s/^\$//;
+ $self->{'root'}->get($path, '$', $query);
+ return $query;
+};
+
+sub DESTROY {
+ my ($self) = @_;
+ #print "DESTROY $self\n";
+ $self->{'root'}->release() if defined($self->{'root'});
+ delete $self->{'root'};
+};
+
+package JsonMod::JSON::Path::Node;
+
+use strict;
+use warnings;
+use utf8;
+use Text::Balanced qw ( extract_codeblock extract_delimited );
+
+sub new {
+ my ($class, $o, $root) = @_;
+
+ my $t = ref($o);
+ if ($t eq 'HASH') {
+ return JsonMod::JSON::Path::HNode->new($o, $root);
+ } elsif ($t eq 'ARRAY') {
+ return JsonMod::JSON::Path::ANode->new($o, $root);
+ } elsif ($t eq '') {
+ return JsonMod::JSON::Path::VNode->new($o, $root);
+ };
+};
+
+sub getNextProperty {
+ my ($self, $path) = @_;
+
+ my ($property, $deep);
+ $deep = $path =~ s/^\.\.//;
+ $path =~ s/^([^\.])/\.$1/;
+ ($path =~ s/^\.([^\[\.]+)// and $property = $1); # .property
+ if (not defined($property)) {
+ $property = extract_codeblock($path, '[]', '\.') and
+ $property = substr($property, 1, (length($property)-2));
+ if (defined($property) and ord($property) eq ord(qw ( ' ))) {
+ $property = extract_delimited($property, qw ( ' ))
+ and $property = substr($property, 1, (length($property)-2));
+ };
+ };
+ return ($path, $property, $deep);
+};
+
+sub addRootNode {
+ my ($self, $o, $root) = @_;
+ if (not $root) {
+ $self->{'root'} = $self;
+ } else {
+ $self->{'root'} = $root;
+ };
+ return $self;
+};
+
+sub release {
+ my ($self) = @_;
+ if (ref($self->{'child'}) eq 'HASH') {
+ foreach my $k (keys %{$self->{'child'}}) {
+ $self->{'child'}->{$k}->release() if defined($self->{'child'}->{$k});
+ delete $self->{'child'}->{$k};
+ };
+ };
+ delete $self->{'root'};
+};
+
+sub DESTROY {
+ my ($self) = @_;
+ #print "DESTROY $self\n";
+};
+
+package JsonMod::JSON::Path::HNode;
+
+use strict;
+use warnings;
+use utf8;
+use parent -norequire, qw( JsonMod::JSON::Path::Node );
+
+sub new {
+ my ($class, $o, $root) = @_;
+
+ my $self = bless {}, $class;
+ #print "HNode $self\n";
+ $self->addRootNode($o, $root);
+
+ foreach my $k (keys %{$o}) {
+ $self->{'child'}->{$k} = JsonMod::JSON::Path::Node->new($o->{$k}, $self->{'root'});
+ };
+
+ return $self;
+};
+
+sub get {
+ my ($self, $path, $normalized, $query) = @_;
+ my ($property, $deep);
+ #print "hash1 [$path] [$property] [$normalized]\n";
+ ($path, $property, $deep) = $self->getNextProperty($path);
+ #print "hash2 [$path] [$property] [$normalized]\n";
+
+ if ((ord($property) eq ord('*')) or $deep) {
+ my @childList = keys (%{$self->{'child'}});
+ foreach my $child (@childList) {
+ $self->getSingle($child, $property, $deep, $path, $normalized, $query);
+ };
+ } else {
+ $self->getSingle($property, $property, $deep, $path, $normalized, $query);
+ };
+};
+
+sub getSingle {
+ my ($self, $node, $property, $deep, $path, $normalized, $query) = @_;
+ #print "hash single: $node, $property, $deep, $path, $normalized\n";
+
+ #$path = "..$property$path" if $deep;
+ if ((ord($property) eq ord('*')) or (($node eq $property) and exists($self->{'child'}->{$node}))) {
+ if (not $path) {
+ #print "hash result $normalized.[$node]\n";
+ $query->addResult($normalized."[$node]", $self->{'child'}->{$node}->getValue());
+ };
+ #$path = "..$property$path" if $deep;
+ if ($path and
+ (not ref($self->{'child'}->{$node}) eq 'JsonMod::JSON::Path::VNode')) {
+ $self->{'child'}->{$node}->get($path, $normalized."[$node]", $query);
+ };
+ };
+ if ($deep) { #and (not ref($self->{'child'}->{$node}) eq 'JsonMod::JSON::Path::VNode')) {
+ $path = "..$property$path";
+ $self->{'child'}->{$node}->get($path, $normalized."[$node]", $query);
+ };
+};
+
+sub getValue {
+ my ($self) = @_;
+ my $val = {};
+ foreach my $c (keys %{$self->{'child'}}) {
+ $val->{$c} = $self->{'child'}->{$c}->getValue();
+ }
+ return $val;
+};
+
+package JsonMod::JSON::Path::ANode;
+
+use strict;
+use warnings;
+use utf8;
+use parent -norequire, qw( JsonMod::JSON::Path::Node );
+
+sub new {
+ my ($class, $o, $root) = @_;
+
+ my $self = bless {}, $class;
+ #print "ANode $self\n";
+ $self->addRootNode($o, $root);
+
+ for my $i (0 .. scalar(@{$o}) -1) {
+ $self->{'child'}->{$i} = JsonMod::JSON::Path::Node->new($o->[$i], $self->{'root'});
+ };
+
+ return $self;
+};
+
+sub get {
+ my ($self, $path, $normalized, $query) = @_;
+ my ($property, $deep);
+ ($path, $property, $deep) = $self->getNextProperty($path);
+
+ if (ord($property) eq ord('?')) {
+ my $filter = JsonMod::JSON::Path::Query::Filter->new($self)->get($property);
+ foreach my $child (sort { $a <=> $b } @{$filter}) {
+ $self->getSingle($child, $child, $deep, $path, $normalized, $query);
+ #$self->{'child'}->{$child}->get($path, $normalized, $query);
+ };
+ } elsif ((ord($property) eq ord('*')) or $deep) {
+ my @childList = sort { $a <=> $b } keys (%{$self->{'child'}});
+ foreach my $child (@childList) {
+ $self->getSingle($child, $property, $deep, $path, $normalized, $query);
+ };
+ } elsif ($property =~ /^\d+$/) {
+ $self->getSingle($property, $property, $deep, $path, $normalized, $query);
+ } else {
+ die ("JsonPath filter property $property failure");
+ };
+};
+
+sub getSingle {
+ my ($self, $node, $property, $deep, $path, $normalized, $query) = @_;
+ #print "array single: $node, $property, $deep, $path, $normalized\n";
+
+ #$path = "..$property$path" if $deep;
+ if ((ord($property) eq ord('*')) or (($node eq $property) and exists($self->{'child'}->{$node}))) {
+ if (not $path) {
+ #print "array result $normalized.[$node]\n";
+ $query->addResult($normalized."[$node]", $self->{'child'}->{$node}->getValue());
+ };
+ #$path = "..$property$path" if $deep;
+ if ($path and
+ (not ref($self->{'child'}->{$node}) eq 'JsonMod::JSON::Path::VNode')) {
+ $self->{'child'}->{$node}->get($path, $normalized."[$node]", $query);
+ };
+ };
+ if ($deep) { #and (not ref($self->{'child'}->{$node}) eq 'JsonMod::JSON::Path::VNode')) {
+ $path = "..$property$path";
+ $self->{'child'}->{$node}->get($path, $normalized."[$node]", $query);
+ };
+};
+
+sub getValue {
+ my ($self) = @_;
+ my $val = [];
+ my @childList = sort { $a <=> $b } keys (%{$self->{'child'}});
+ foreach my $c (@childList) {
+ push @{$val}, $self->{'child'}->{$c}->getValue();
+ }
+ return $val;
+};
+
+package JsonMod::JSON::Path::VNode;
+
+use strict;
+use warnings;
+use utf8;
+use parent -norequire, qw( JsonMod::JSON::Path::Node );
+
+sub new {
+ my ($class, $o, $root) = @_;
+
+ my $self = bless {}, $class;
+ #print "VNode $self\n";
+ $self->addRootNode($o, $root);
+
+ if (not $root) {
+ $root = $self->{'root'} = $o;
+ } else {
+ $self->{'root'} = $root;
+ };
+ $self->{'child'} = $o;
+ return $self;
+};
+
+sub get {
+ my ($self, $path, $normalized) = @_;
+ my ($property, $deep);
+ ($path, $property, $deep) = $self->getNextProperty($path);
+};
+
+sub getValue {
+ my ($self) = @_;
+ return $self->{'child'};
+};
+
+package JsonMod::JSON::Path::Query;
+
+use strict;
+use warnings;
+use utf8;
+
+sub new {
+ my ($class) = @_;
+ my $self = bless {}, $class;
+ $self->{'nList'} = [];
+ $self->{'vList'} = [];
+ return $self;
+};
+
+sub addResult {
+ my ($self, $normalized, $value) = @_;
+ push @{$self->{'nList'}}, $normalized;
+ push @{$self->{'vList'}}, $value;
+};
+
+sub getResultNormalized {
+ my ($self) = @_;
+ foreach my $e (@{$self->{'nList'}}) {
+ print "$e\n";
+ };
+
+};
+
+sub getResultValue {
+ my ($self) = @_;
+ return $self->{'vList'};
+};
+
+sub getResultNormVal {
+ my ($self) = @_;
+ for my $i (0 .. scalar(@{$self->{'vList'}}) -1) {
+ print "$self->{'nList'}->[$i]\t$self->{'vList'}->[$i]\n";
+ };
+};
+
+sub getResultList {
+ my ($self) = @_;
+ my $result = [];
+ for my $i (0 .. scalar(@{$self->{'vList'}}) -1) {
+ push @{$result}, [$self->{'nList'}->[$i], $self->{'vList'}->[$i]];
+ };
+ return $result;
+};
+
+package JsonMod::JSON::Path::Query::Filter;
+
+use strict;
+use warnings;
+use utf8;
+use List::Util qw( any );
+use Text::Balanced qw ( extract_codeblock extract_delimited );
+
+sub new {
+ my ($class, $o) = @_;
+ my $self = bless {}, $class;
+ $self->{'nList'} = [];
+ $self->{'vList'} = [];
+ $self->{'node'} = $o;
+ return $self;
+};
+
+sub get {
+ my ($self, $filterText) = @_;
+ my $filter;
+ $filter = extract_codeblock($filterText, '()', '\?')
+ and $filter = substr($filter, 1, (length($filter)-2));
+
+ my ($delim, $list, $idx) = (0, 0, 0);
+ my @parts;
+ foreach my $c (split '', $filter) {
+ $delim ^= 1 if (ord($c) == ord(q{'}));
+ $list += 1 if (ord($c) == ord('[') and $delim == 0);
+ $list -= 1 if (ord($c) == ord(']') and $delim == 0);
+ die('unbalanced square brackets in JsonPath filter: '.$filterText) if ($list < 0);
+ $idx++ if (ord($c) == ord(' ') and $delim == 0 and $list == 0);
+ $parts[$idx] .= $c if (ord($c) != ord(' ') or $list != 0 or $delim == 1);
+ };
+ die('unbalanced square brackets in JsonPath filter: '.$filterText) if ($list != 0);
+ return $self->filter($parts[0], $parts[1], $parts[2]);
+
+};
+
+sub filter {
+ my ($self, $left, $operater, $right) = @_;
+
+ my $result = [];
+
+ # fn ref as test for: numeric, string, list
+ my ($a, $b, @a, @b);
+ my $dispatch = {
+ '==' => [sub {$a == $b}, sub {$a eq $b}, undef],
+ '!=' => [sub {$a != $b}, sub {$a ne $b}, undef],
+ '<' => [sub {$a < $b}, sub {$a lt $b}, undef],
+ '<=' => [sub {$a <= $b}, sub {$a le $b}, undef],
+ '>' => [sub {$a > $b}, sub {$a gt $b}, undef],
+ '>=' => [sub {$a >= $b}, sub {$a ge $b}, undef],
+ 'in' => [undef, undef, sub {any {$_ eq $a} @b}],
+ };
+
+ # todo: test if right is filter!!!
+
+ # right type == numeric, string, list / operater as string / function pointer
+ my ($fnt, $fn);
+ ($right =~ m/([+-]?\d+(?:[,.]\d+)?)/ and $fnt = 0) or # numeric
+ ($right =~ m/^(?:['](.*)['])$/ and $fnt = 1) or # string
+ ($right =~ m/^(?:[\[](.*)[\]])$/ and $fnt = 2); # list
+ $right = $1 if (defined($fnt));
+ $fn = exists($dispatch->{$operater})?$dispatch->{$operater}->[$fnt]:undef;
+ if ($fn) {
+ # run query
+ my $filterpath = $left;
+ my $queryNode;
+ if ($filterpath =~ s/^([\$\@])\./[*]/) {
+ $queryNode = $self->{'node'} if ($1 eq '@');
+ $queryNode = $self->{'node'}->{'root'} if ($1 eq '$');
+ } else {
+ die("JsonPath filter '$left' must start with \@. or \$.");
+ };
+ my $filter = JsonMod::JSON::Path::Query->new();
+ my $fltNormalized = ''; # relative to actual node
+ $queryNode->get($filterpath, $fltNormalized, $filter);
+ my $list = $filter->getResultList();
+
+ # numeric or string
+ if ($fnt == 0 or $fnt == 1) {
+ foreach my $e (@{$list}) {
+ $a = $e->[1]; # -> val
+ $b = $right;
+ if ($fn->()) { # call the test
+ my $r = extract_codeblock($e->[0], '[]');
+ push @{$result}, substr($r, 1, length($r) - 2); # remove []
+ };
+ };
+ # list
+ } elsif ($fnt == 2) {
+ foreach (split /,/, $right) {
+ s/^\s*'|^\s+|'\s+|'\s*$//g;
+ push @b, $_;
+ };
+ foreach my $e (@{$list}) {
+ $a = $e->[1]; # -> val
+ if ($fn->()) { # call the test
+ my $r = extract_codeblock($e->[0], '[]');
+ push @{$result}, substr($r, 1, length($r) - 2); # remove []
+ };
+ };
+ };
+ };
+
+ return $result;
+};
+
+sub DESTROY {
+ my ($self) = @_;
+ delete $self->{'node'};
+};
+
+package JsonMod::Cron;
+
+use strict;
+use warnings;
+use utf8;
+use Time::Local qw ( timelocal );
+
+# static and helper
+sub normalizeTime {
+ my ($m, $h, $d) = @_;
+ $d //= 0;
+ if ($m > 59) { $h += int($m / 60); $m %= 60; };
+ if ($h > 23) { $d += int($h / 24); $h %= 24; };
+ return ($m, $h, $d);
+};
+
+sub normalizeDate {
+ my ($d, $m, $y, $o) = @_;
+ $o //= 0;
+ my $time = timelocal(0, 0, 12, $d, $m -1, $y -1900);
+ $time += $o * 86400;
+ my @t = localtime($time);
+ # plus DST, wday (SUN=0..6), yday (0..364|5)
+ return ($t[3], $t[4] +1, $t[5] +1900, $t[8], $t[6], $t[7]);
+};
+
+# class
+sub new {
+ my ($class) = @_;
+ my $self = {};
+
+ bless $self, $class;
+ return $self;
+};
+
+sub setCron {
+ my ($self, $cron) = @_;
+ @{$self->{'CRONLIST'}} = split / /, $cron //= '';
+ return if (scalar @{$self->{'CRONLIST'}} != 5);
+
+};
+
+sub parseMinuteEntry {
+ my ($self, $in, $now) = @_;
+ my ($res, $start, $stop, $step);
+
+ ($step) = ($in =~ m/\/([0-9]|[0-5][0-9])$/);
+ ($start, $stop) = ($in =~ m/^([*]|[0-9]|[0-5][0-9])(?:-([0-9]|[0-5][0-9]))?(?:\/(?:[0-9]|[0-5][0-9]))?$/);
+ return if (not defined($start) or ($start eq '*' and defined($stop))); # syntax error
+
+ $stop = (defined($step) or ($start eq '*'))?59:$start if (not defined($stop));
+ $start = 0 if $start eq '*';
+ return if ($start > $stop); # syntax error
+ return $start if ($now < $start); # literal start
+
+ $res = $step //= 1;
+ $res = $res - (((($now - $start) % 60) + $res) % $res);
+ $res = $now + $res;
+
+ return $start + 60 if ($res > $stop); # carry over
+ return $res; # regular next
+};
+
+sub parseHourEntry {
+ my ($self, $in, $now) = @_;
+ my ($res, $start, $stop, $step);
+
+ ($step) = ($in =~ m/\/([0-9]|[0,1][0-9]|2[0-3])$/);
+ ($start, $stop) = ($in =~ m/^([*]|[0-9]|[0,1][0-9]|2[0-3])(?:-([0-9]|[0,1][0-9]|2[0-3]))?(?:\/(?:[*]|[0-9]|[0,1][0-9]|2[0-3]))?$/);
+ return if (not defined($start) or ($start eq '*' and defined($stop))); # syntax error
+
+ $stop = (defined($step) or ($start eq '*'))?23:$start if (not defined($stop));
+ $start = 0 if $start eq '*';
+ return if ($start > $stop); # syntax error
+ return $start if ($now < $start); # literal start
+
+ $res = $step //= 1;
+ $res = ($now - $start) % $res;
+
+ return $now if ($res == 0) and ($now <= $stop); # current hour
+ $res = $now + $step - $res;
+ return $start + 24 if ($res > $stop); # carry over
+ return $res; # regular next
+};
+
+sub parseDateEntry {
+ my ($self, $in, $now) = @_;
+ my ($res, $start, $stop, $step);
+
+ ($step) = ($in =~ m/\/([0-9]|[0-2][0-9]|3[0,1])$/);
+ ($start, $stop) = ($in =~ m/^([*]|[0-9]|[0-2][0-9]|3[0,1])(?:-([0-9]|[0-2][0-9]|3[0,1]))?(?:\/(?:[*]|[0-9]|[0-2][0-9]|3[0,1]))?$/);
+ return if (not defined($start) or ($start eq '*' and defined($stop))); # syntax error
+
+ $stop = (defined($step) or ($start eq '*'))?31:$start if (not defined($stop));
+ $start = 1 if $start eq '*';
+ return if ($start > $stop); # syntax error
+ return $start if ($now < $start); # literal start
+
+ $res = $step //= 1;
+ $res = ($now - $start) % $res;
+
+ return $now if ($res == 0) and ($now <= $stop); # current
+ $res = $now + $step - $res;
+ return $start + 32 if ($res > $stop); # carry over
+ return $res; # regular next
+};
+
+sub next {
+ my ($self, $cron, @t) = @_;
+
+ my $inDay = sprintf('%04d%02d%02d', $t[5], $t[4], $t[3]);
+ my ($cronMin, $cronHour, $cronDay, $cronMonth, $cronWeekDay) = split / /, $cron;
+ my ($time, $dst, $weekday);
+
+ # m h d(carry)
+ $time = $self->nextTime($t[1], $t[2], $cronMin, $cronHour);
+ return if (not $time);
+ ($t[3], $t[4], $t[5], $dst, $weekday) = normalizeDate($t[3], $t[4], $t[5], $time->[2]);
+ my $calcDay = sprintf('%04d%02d%02d', $t[5], $t[4], $t[3]);
+
+ # date unchanged and known
+ if ($calcDay eq $inDay) {
+ return ($time->[0], $time->[1], $t[3], $t[4], $t[5], $dst);
+ };
+
+ # m h d(carry)
+ $time = $self->nextTime(0, 0, $cronMin, $cronHour);
+ #($t[3], $t[4], $t[5], $dst, $weekday) = normalizeDate($t[3], $t[4], $t[5], $time->[2]);
+
+ # yyyy mm dd
+ my $date = $self->nextDate($t[3], $t[4], $t[5], $cronDay, $cronMonth);
+ return if (not $date);
+ ($t[3], $t[4], $t[5], $dst, $weekday) = normalizeDate($date->[2], $date->[1], $date->[0]);
+
+ return ($time->[0], $time->[1], $t[3], $t[4], $t[5], $dst);
+};
+
+# test if valid cron expression
+sub validate {
+ my ($self, $cron) = @_;
+ my ($cronMin, $cronHour, $cronDay, $cronMonth, $cronWeekDay) = split / /, $cron;
+ my $time = $self->nextTime(0, 0, $cronMin, $cronHour);
+ my $date = $self->nextDate(2020, 1, 1, $cronDay, $cronMonth);
+ if (defined($time) and defined($date)) {
+ return 1;
+ } else {
+ return;
+ };
+};
+
+# min = time: actual minute
+# hour = time: actual hour
+sub nextTime {
+ my ($self, $min, $hour, $cronMin, $cronHour) = @_;
+
+ my $calcMin;
+ my $calcHour;
+ my $calcDay = 0;
+
+ foreach my $cronMinEntry (split /,/, $cronMin) {
+ my $e = $self->parseMinuteEntry($cronMinEntry, $min);
+ return if not defined($e); # syntax error
+ if ((not defined($calcMin) and defined($e)) or ($e < $calcMin)) {
+ $calcMin = $e;
+ };
+ };
+ ($calcMin, $hour, $calcDay) = normalizeTime($calcMin, $hour, $calcDay);
+
+ foreach my $cronHourEntry (split /,/, $cronHour) {
+ my $e = $self->parseHourEntry($cronHourEntry, $hour);
+ return if not defined($e); # syntax error
+ if ((not defined($calcHour) and defined($e)) or ($e < $calcHour)) {
+ $calcHour = $e;
+ };
+ };
+ my (@time) = normalizeTime($calcMin, $calcHour, $calcDay);
+ return \@time;
+
+};
+
+sub nextDate {
+ my ($self, $day, $month, $year, $cronDay, $cronMonth) = @_;
+
+ my $dates = $self->listDates($day, $month, $year, $cronDay, $cronMonth);
+ my $result;
+ foreach (@{$dates}) {
+ if ((not defined($result) and defined($_)) or ($_ and ($_ < $result))) {
+ $result = $_;
+ };
+ };
+ return if (not defined($result));
+ my (@date) = ($result =~ m/^(\d{4})(\d{2})(\d{2})$/);
+ return \@date;
+};
+
+sub listDates {
+ my ($self, $day, $month, $year, $cronDay, $cronMonth) = @_;
+ my @result;
+
+ #return [] if ($self->{R}++ > 25);
+
+ my sub daysOfMonth {
+ my ($m, $y) = @_;
+ my (@d) = (0,31,28,31,30,31,30,31,31,30,31,30,31);
+ # leapyear
+ $d[2] = 29 if (((($y % 4) == 0) and (($y % 100) != 0)) or (($y % 400) == 0));
+ return ($d[$m]);
+ };
+
+ foreach my $cronDayEntry (split /,/, $cronDay) {
+ foreach my $cronMonthEntry (split /,/, $cronMonth) {
+ # impossible cron would recurse forever: [31 2 * * *] / [31 9/2 * * *]
+ my $invalid = 1;
+ if ((my ($fuseDay) = ($cronDayEntry =~ m/^(\d{1,2})/)) and
+ (my ($fuseMonth, $fuseMonthStep) = ($cronMonthEntry =~ m/^(\d{1,2})(?:\/(\d{1,2}))*/))) {
+ #print "FUSE $fuseDay, $fuseMonth, $fuseMonthStep\n";
+ for (my $i = $fuseMonth; $i <= 12 and $invalid; $i += $fuseMonthStep //= 12) {
+ $invalid = 0 if (daysOfMonth($fuseMonth, 2000) >= $fuseDay); # 2000 is leapyear
+ };
+ if ($invalid) {
+ push @result, ();
+ next;
+ };
+ };
+ my $calcDay = $self->parseDateEntry($cronDayEntry, $day);
+ my $calcMonth = $self->parseDateEntry($cronMonthEntry, $month);
+ my $calcYear = $year;
+ #printf "Test: D:%s, M:%s against %s-%s -> %s-%s-%s\n", $cronDayEntry, $cronMonthEntry, $day, $month, $calcDay, $calcMonth, $calcYear;
+ if (defined($calcDay) and defined($calcMonth)) {
+ #$doy = isValid($testM, $testMd);
+ if (($calcDay == $day) and ($calcMonth == $month)) {
+ #printf "RETURN: D:%s, M:%s against %s-%s-%s -> %s-%s-%s\n", $cronDayEntry, $cronMonthEntry, $day, $month, $year, $calcDay, $calcMonth, $calcYear;
+ push @result, sprintf('%04d%02d%02d', $calcYear, $calcMonth, $calcDay);
+ } else {
+ if ($calcMonth > 12) {
+ $calcMonth -= ($calcMonth == 13)?12:32;
+ $calcYear++;
+ };
+ if ($calcDay > daysOfMonth($calcMonth, $calcYear)) {
+ $calcMonth++ if ($calcMonth == $month);
+ $calcDay = 1;
+ };
+ push @result, @{ $self->listDates($calcDay, $calcMonth, $calcYear, $cronDayEntry, $cronMonthEntry) };
+ };
+ } else {
+ return []; # syntax error
+ };
+ };
+ };
+ return \@result;
+};
+
+1;
+
+=pod
+=item helper
+=item summary provides a generic way to parse and display json source
+=item summary_DE JSON Quellen parsen und und verwenden
+=begin html
+
+
+
JsonMod
+
+ JsonMod provides a generic way to load and parse json files from HTTP sources periodically.
+ Elements within the json files can be selected and displayed in a targeted manner.
+
+ JsonMod uses the JsonPath syntax to access elements or lists within the json file.
+ The well-known cron syntax is used for the periodic retrieval of the files.
+
+
+
+ Define
+
+ define <name> JsonMod <http[s]:example.com:/somepath/somefile.json>
+
+ defines the device and set the source
+
+
+
+
+ Set
+
+ - secret
+
+ set <name> secret <identifier> <value>
+
+ To prevent the leakage of sensitive information, like credentials or api keys,
+ they can be stored separate and thus are not shown neither in the config file nor in listings.
+ Access to that information is provided by putting square brackets and the identifier [identifier]
+ into the http source within the definition or in a http header (see attribute).
+
+
+
+
+
+
+ Get
+
+
+
+
+ Attributes
+
+
+ - interval
+ set <name> interval <*/15 * * * *>
+ utilize a cron expression to define the interval at which the source file will be loaded.
+ Default is one hour.
+
+
+ - readingList
+ Specifies the access to json elements and their representation as well as formatting as reading.
+ In its conventions, the syntax follows normal perl expression but uitlies a special set of instructions.
+ This means that an expression must end with a semicolon, parentheses must be equal, and be of the correct type.
+ When using double quotes, the content is interpolated. Since Jsonpath uses the '$' and '@' characters as part of the syntax,
+ they must be escaped in expressions within double quotes. It is therefore preferable to use single quotes wherever possible.
+
+ Recognized expressions (where '$.' is a placeholder for a valid json path expression):
+
+ -
+ single(jsonPath('$.'), 'readingname', 'default value');
+ creates one reading. The json path expression must translate into a value (not into an array or an object)
+
+ -
+ multi(jsonPath('$.'), <Instructions for creating the reading name>, <property>);
+ creates multiple (0..n) readings. Jsonpath expression must translate into an array of objects or values.
+ Because the number of readings is variable, a function is used to generate the reading names.
+ Typically, this is based on the index of the array element and / or a property of the addressed objects.
+
+ -
+ jsonPath('$.');
+ Creates a jsonpath expression as part of a 'single' or 'multi' expression.
+
+ -
+ jsonPathf('$.', 'format');
+ Creates a jsonpath expression as part of a 'single' expression and format its result.
+ The syntax of the 'format' expression Mimics the syntax of printf.
+
+ -
+ concat('expression', 'expression', ...);
+ Concatenates the expressions to one result.
+ Can be used in a 'multi ()' statement to create a reading name from one or more object properties or the index.
+
+ -
+ index();
+ Contains the index number of the current list element.
+ Within 'multi ()' instructions for generating reading names using 'connect ()' are used.
+
+
+
+
+
+
+=end html
+
+=cut
\ No newline at end of file