From 475b38aaee4d476c752a865272ce7ade784d1e1a Mon Sep 17 00:00:00 2001 From: Benni <> Date: Sat, 2 Mar 2024 15:58:29 +0000 Subject: [PATCH] 02_FHEMAPP.pm: New Companion-Module for FHEMApp (Forum #137239) git-svn-id: https://svn.fhem.de/fhem/trunk@28581 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/02_FHEMAPP.pm | 1517 +++++++++++++++++++++++++++++++++++++++ fhem/MAINTAINER.txt | 1 + 2 files changed, 1518 insertions(+) create mode 100644 fhem/FHEM/02_FHEMAPP.pm diff --git a/fhem/FHEM/02_FHEMAPP.pm b/fhem/FHEM/02_FHEMAPP.pm new file mode 100644 index 000000000..a90232a69 --- /dev/null +++ b/fhem/FHEM/02_FHEMAPP.pm @@ -0,0 +1,1517 @@ +############################################## +# $Id$ +package FHEM::FHEMAPP; + +use strict; +use warnings; +use HttpUtils; + +use MIME::Base64; + +use Data::Dumper; + +use GPUtils qw(:all); + +use File::Temp qw(tempfile tempdir cleanup); +use File::Path qw(rmtree); + +#$dir = File::Temp->newdir(); + +#https://www.perl-howto.de/2008/07/temporare-dateien-sicher-erzeugen.html +#https://metacpan.org/release/TJENNESS/File-Temp-0.22/view/Temp.pm#OBJECT-ORIENTED_INTERFACE + + +######################################################################### +# Importing/Exporting Functions and variables from/to main +######################################################################### +BEGIN { + GP_Import(qw( + defs + AttrVal + ReadingsVal + InternalVal + Log3 + fhem + readingFnAttributes + readingsSingleUpdate + readingsBeginUpdate + readingsBulkUpdateIfChanged + readingsBulkUpdate + readingsEndUpdate + readingsDelete + init_done + FW_CSRF + FW_confdir + FW_dir + FW_ME + CommandAttr + CommandDeleteAttr + devspec2array + getAllSets + getAllAttr + IsIgnored + unicodeEncoding + WriteStatefile + HttpUtils_NonblockingGet + getAllAttr + FileWrite + FileRead + FileDelete + gettimeofday + InternalTimer + RemoveInternalTimer + IsDisabled + deviceEvents + )); + + #Exporting Initialize for Main + GP_Export(qw( + Initialize + )) +} + +######################################################################### +# Trying to import functions from an applicaple JSON-Library +######################################################################### + my $JSON="none"; + #JSON-Library-Usage was cpopied from Weather-APIs + # try to use JSON::MaybeXS wrapper + # for chance of better performance + open code + eval { + require JSON::MaybeXS; + import JSON::MaybeXS qw( decode_json encode_json ); + $JSON='JSON::MaybeXS'; + 1; + } or do { + + # try to use JSON wrapper + # for chance of better performance + eval { + # JSON preference order + local $ENV{PERL_JSON_BACKEND} = + 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' + unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); + + require JSON; + import JSON qw( decode_json encode_json ); + $JSON='JSON'; + 1; + } or do { + + # In rare cases, Cpanel::JSON::XS may + # be installed but JSON|JSON::MaybeXS not ... + eval { + require Cpanel::JSON::XS; + import Cpanel::JSON::XS qw(decode_json encode_json); + $JSON='Cpanel::JSON::XS'; + 1; + } or do { + + # In rare cases, JSON::XS may + # be installed but JSON not ... + eval { + require JSON::XS; + import JSON::XS qw(decode_json encode_json); + $JSON='JSON::XS'; + 1; + } or do { + + # Fallback to built-in JSON which SHOULD + # be available since 5.014 ... + eval { + require JSON::PP; + import JSON::PP qw(decode_json encode_json); + $JSON='JSON::PP'; + 1; + } or do { + + # Fallback to JSON::backportPP in really rare cases + require JSON::backportPP; + import JSON::backportPP qw(decode_json encode_json); + $JSON='JSON::backportPP'; + 1; + }; + }; + }; + }; + }; + +######################################################################### +# Constants and defaults +######################################################################### + use constant { + FA_VERSION => '1.0.0', #Version of this Modul + FA_VERSION_FILENAME => 'CHANGELOG.md', #Default Version Filename + FA_INIT_INTERVAL => 60, #Default Startup Interval + FA_DEFAULT_INTERVAL => 3600, #Default Interval + FA_GITHUB_URL => 'https://github.com/jemu75/fhemApp', + FA_GITHUB_API_BASEURL => 'https://api.github.com/repos/jemu75/fhemApp', + FA_GITHUB_API_OWNER => 'jemu75', + FA_GITHUB_API_REPO => 'fhemApp', + FA_GITHUB_API_RELEASES => 'releases', + FA_TAR_SUB_FOLDER => 'www/fhemapp4', + FA_VERSION_LOWEST => '4.0.0', + FA_MOD_TYPE => (split('::',__PACKAGE__))[-1], + INT_SOURCE_URL => 'SOURCE_URL', #INTERNAL NAME + INT_CONFIG_FILE => 'CONFIG_FILE', #INTERNAL NAME + INT_INTERVAL => 'INTERVAL', #INTERNAL NAME + INT_VERSION => 'VERSION', #INTERNAL NAME + INT_JSON_LIB => '.JSON_LIB', #INTERNAL NAME + INT_LOCAL_INST => 'LOCAL', #INTERNAL NAME + INT_PATH => 'PATH', #INTERNAL NAME + INT_LINK => 'FHEMAPP_UI', #INTERNAL NAME + INT_FANAME => 'FHEMAPP_NAME' #INTERNAL NAME + }; + no warnings 'qw'; + + my @attrList = qw( + disable:1,toggle + interval + sourceUrl + updatePath:beta + ); + + # autoUpdate:1 + + use warnings 'qw'; + + + +######################################################################### +# FHEM - Module management Functions (xxxFn) +######################################################################### + #======================================================================== + sub Initialize + #======================================================================== + { + my $hash=shift // return; + + $hash->{DefFn} = \&Define; + $hash->{GetFn} = \&Get; + $hash->{SetFn} = \&Set; + $hash->{DeleteFn} = \&Delete; + $hash->{CopyFn} = \&DeviceCopied; + $hash->{RenameFn} = \&DeviceRenamed; + $hash->{NotifyFn} = \&Notify; + $hash->{AttrFn} = \&Attr; + + $hash->{AttrList} = join(" ", @attrList)." $readingFnAttributes"; + } + #======================================================================== + sub Attr # AttrFn + #======================================================================== + { + my $cmd= shift // return undef; + my $name=shift // return undef; + my $att= shift // return undef; + my $val= shift; + + my $hash = $defs{$name}; + + #set fa interval 59 + Log($name,"AttrFn: $cmd $name $att $val",4); + if($att eq 'disable') { + if($val && $val==1) { + StopLoop($hash); + readingsSingleUpdate($hash,'state','disabled',1); + + } + elsif ( $cmd eq "del" or !$val ) { + readingsSingleUpdate($hash,'state','defined',1); + StartLoop($hash,FA_INIT_INTERVAL,0,1); + } + } + elsif($att eq 'interval') { + if($cmd eq 'set') { + $val+=0; + if($val < FA_INIT_INTERVAL) { + $val=0; + #will be disabled if < 60 seconds => set to 0 + return "Interval should not be set lower than ".FA_INIT_INTERVAL; + } + elsif($val > 86400) { + return "Interval should not be longer than 1 day (86400 sec)"; + } + elsif($val==FA_DEFAULT_INTERVAL) { + return "Default interval is already $val"; + } + $hash->{&INT_INTERVAL}=$val; + StartLoop($hash,FA_INIT_INTERVAL,1); + } + else + { + $hash->{&INT_INTERVAL}=FA_DEFAULT_INTERVAL; + StartLoop($hash,FA_INIT_INTERVAL,1); + } + } + elsif($att eq 'sourceUrl') { + if($cmd eq 'set') { + if($val eq FA_GITHUB_URL) { + return "$val is already default for SourceUrl"; + } + $hash->{&INT_SOURCE_URL}=$val; + } + else + { + $hash->{&INT_SOURCE_URL}=FA_GITHUB_URL; + } + } + return undef; + } + + #======================================================================== + sub Notify # NofifyFn + #======================================================================== + { + my $hash = shift // return; + my $src_hash=shift // return; + + + my $name = $hash->{NAME}; + return if(IsDisabled($name)); + + my $src = $src_hash->{NAME}; + + my $events = deviceEvents($src_hash,1); + return if( !$events ); + + #HANDLING GLOBAL EVENTS + if($src eq 'global') { + foreach my $event (@{$events}) { + Log($name,"EVENT: $src:$event",5); + $event = "" if(!defined($event)); + if($event eq 'INITIALIZED') { + Log($name,"Event recieved from '$src'",5); + StartLoop($hash,FA_INIT_INTERVAL); + } + } + } + return; + } + + #======================================================================== + sub Define # DefFn + #======================================================================== + { + my $hash=shift // return; + + my $def=shift; + my @a = split("[ \t][ \t]*", $def); + + my $name=shift @a; + my $type=shift @a; + my $fa_name= shift @a; + + Log(undef,"DefFn called for $name $init_done",4); + + + #Setting INTERNAL values + $hash->{&INT_VERSION}=FA_VERSION; + $hash->{&INT_SOURCE_URL}=AttrVal($name,'sourceUrl',FA_GITHUB_URL); + $hash->{&INT_JSON_LIB}=$JSON; + $hash->{&INT_CONFIG_FILE}=get_config_file($name); + $hash->{&INT_INTERVAL}=AttrVal($name,'interval',FA_DEFAULT_INTERVAL); + $hash->{&INT_FANAME}=$fa_name; + + $hash->{NOTIFYDEV}="global"; + + #Internal PATH is only available if local path is specified in DEF + if($fa_name eq 'none') { + delete($hash->{&INT_PATH}); + $hash->{&INT_LOCAL_INST}=0; + } else { + $hash->{&INT_PATH}="$FW_dir/$fa_name"; + $hash->{&INT_LOCAL_INST}=1; + } + + set_fhemapp_link($hash); + + #Setting defined state + if(!$init_done) { + #Reading conifg on Define, e.g. when copied from other device + #[todo]: this is not necessary on simple define .... Move to CopyFn? + ReadConfig($hash); + } + else + { + #Start Version loop + #[todo]: maybe move to global:Initialize handling in NotifyFn + StartLoop($hash,FA_INIT_INTERVAL); + } + + #Set startup disabled/defined state + if(IsDisabled($name)) { + readingsSingleUpdate($hash,'state','disabled',0); + } + else + { + readingsSingleUpdate($hash,'state','defined',0); + } + + return "Wrong syntax: use define fhemapp " if(!$fa_name); + } + + #======================================================================== + sub Get # GetFn + #======================================================================== + { + my $hash=shift // return; + my $name=shift; + my $opt=shift; + + return "\"get $name\" needs at least one argument" unless(defined($opt)); + + if($opt eq 'config') { + return encode_base64(get_config($hash)); + } + elsif($opt eq 'rawconfig') { + return get_config($hash); + } + elsif($opt eq 'options') { + return AttrOptions_json($name); + } + elsif($opt eq 'version') { + check_local_version($hash); + return ReadingsVal($name,'local_version','unknown'); + #return get_local_version($hash); + } + elsif($opt eq 'localfolder') { + return get_local_path($hash); + } + else + { + #my $loc_gets='version:noArg'; + my $loc_gets=''; + if(get_local_path($hash)) { + return "Unknown argument $opt, choose one of rawconfig:noArg $loc_gets"; + } else { + return "Unknown argument $opt, choose one of rawconfig:noArg"; + } + } + } + + #======================================================================== + sub Set # SetFn + #======================================================================== + { + my $hash=shift // return; + my $name=shift; + my $opt=shift; + + my @args=@_; + + return "\"set $name\" needs at least one argument" unless(defined($opt)); + + if($opt eq 'config') { + set_config($hash,decode_base64(join(" ",@args)),1); + } + elsif($opt eq 'rawconfig') { + set_config($hash,join(" ",@args),1); + } + elsif($opt eq 'update') { + update($hash); + } + elsif($opt eq 'forceVersion') { + return forceVersion($hash,@args); + } + elsif($opt eq "checkVersions") { + check_local_version($hash); + Request_Releases($hash); + } + elsif($opt eq "createfolder") { + create_fhemapp_folder($hash); + } + elsif($opt eq "refreshLink") { + set_fhemapp_link($hash); + } + elsif($opt eq "rereadCfg") { + ReadConfig($hash,1); + } + else { + return "Unknown argument $opt, choose one of checkVersions:noArg update:noArg rereadCfg:noArg"; + } + return undef; + } + + #======================================================================== + sub Delete # DeleteFn + #======================================================================== + #Cleanup when device is deleted + { + my $hash=shift // return; + + #Cancel and delete runnint internal timer + StopLoop($hash); + #Delete config file + DeleteConfig($hash); + } + + #======================================================================== + sub DeviceCopied #CopyFn + #======================================================================== + { + my $old_name=shift // return; + my $new_name=shift // return; + + Log ($new_name,"Copying config '$old_name' -> '$new_name'",4); + $defs{$new_name}{helper}{config} = $defs{$old_name}{helper}{config}; + #Log($new_name,$new_hash->{helper}{config},5); + + WriteConfig($defs{$new_name}); + + return; + } + + #======================================================================== + sub DeviceRenamed #RenameFn + #======================================================================== + { + my $new_name=shift // return; + my $old_name=shift // return; + + Log($new_name,"Device renamed '$old_name' -> '$new_name'",4); + my $new_hash=$defs{$new_name}; + WriteConfig($new_hash); + + my $oldFile=get_config_file($old_name); + Log($new_name,"Deleting config '$oldFile' ...",4); + FileDelete($oldFile); + + return; + + } + +#======================================================================== +sub create_fhemapp_folder +#======================================================================== +{ + my $hash=shift // return; + my $name=$hash->{NAME}; + + my $localInst=InternalVal($name,INT_LOCAL_INST,0); + if(!$localInst) { + Log($name,"create_fhemapp_folder: no local fhemapp-instance!",2); + return 0; + } + my $fld=InternalVal($name,INT_PATH,undef); + if(!$fld) { + Log($name,"no path specification found check DEF of $name",2); + return 0; + } + if(-d $fld) { + Log($name,"create_fhemapp_folder: folder already exists: '$fld'",2); + return 0; + } + + mkdir($fld); + if(-d $fld) { + Log($name,"create_fhemapp_folder: folder successfully created: '$fld'",4); + return 1; + } else { + Log($name,"create_chemapp_folder: unable to create folder '$fld'",2); + return 0; + } + + #my $name=shift // return; + #my $destFolder=InternalVal($name,INT_PATH,'none'); +} + +#======================================================================== +sub forceVersion +#======================================================================== +{ + my $hash=shift // return 'no hash'; + my @args=shift // return 'no args'; + + my $first=shift @args // return 'empty args'; + + my $name=$hash->{NAME}; + my $localPath=get_local_path($hash); + + return "$first - $localPath"; + +} + +#======================================================================== +sub update { +#======================================================================== + + my $hash=shift // return; + + my $name = $hash->{NAME}; + + my $continueUpdate = shift; + $continueUpdate //=0; + + #TODO: Get Releases ... this is a non-blocking (async) process ... + # ... need to wait until finished ... non-blocking :( + if(!$continueUpdate) { + Log($name,"Update ... first checking versions ...",4); + check_local_version($hash); + Request_Releases($hash,1); + return; + } else { + Log($name,"Update ... got releases ... continuing...",4); + } + + #Find required tarball-URL + my $url=undef; + my $updatePath=AttrVal($name,'updatePath','stable'); + if( $updatePath eq 'beta') { + $url=ReadingsVal($name,'.pre_tarball_url',undef); + } else { + $url=ReadingsVal($name,'.stable_tarball_url',undef); + } + + #Build non-blocking request to download tarball from github + #Donwload is handled in callback sub 'update_response' + if($url) { + Log($name,"Requesting: $url",4); + my $param = { + url => $url, + timeout => 5, + hash => $hash, + method => "GET", + header => "User-Agent: TeleHeater/2.2.3\r\nAccept: application/json", + callback => \&update_response + }; + HttpUtils_NonblockingGet($param); + } else { + Log($name, "Update: No url for current update-path '$updatePath' available!",4); + } + return; + +} + +#======================================================================== +sub update_response{ +#======================================================================== + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + my $localPath=get_local_path($hash); + + my $fname="fhemapp_update.tar.gz"; + Log($name,"request-header: ".$param->{httpheader},5); + + #Extracting the package filename from httpheader + if($param->{httpheader} =~/filename=(.+.tar.gz)/gm) { + $fname=$1; + } + Log($name,"filename for update package is $fname"); + #{my $v1="ertuy";;if($v1 =~ /er(tu)y/gm) {$1}} + + my @hdr=split('\n',$param->{httpheader}); + Log($name,"http-header:".Dumper(@hdr),5); + + if($err ne "") + { + #An Error occured during request + Log($name,"error while requesting ".$param->{url}." - $err",4); + readingsSingleUpdate($hash, ".fullResponse", "ERROR: $err", 0); + } + elsif($data ne "") + { + #Incoming data ... saving file + Log($name,"update data recieved ".$param->{url},4); + my $dir = tempdir(CLEANUP=>1); + $dir=~ s!/*$!/!; + my $filename="${dir}$fname"; + my @content; + push @content,$data; + + #FileWrite($filename,@content); #-> Added one, unwanted character (probably a \n) + #So doing "native" file-write here: + Log($name,"writing $filename ",4); + open(FH, '>', $filename); + print FH $data; + close(FH); + + #my $topfolder=`tar -tzf $filename' | head -1 | cut -f1 -d"/"`; + #chomp $topfolder; + my $tarlist=`tar -tzf $filename`; + my $topfolder=(split /\n/, $tarlist )[0]; + chop $topfolder if($topfolder =~ /.*\/$/); + Log($name,"top folder in '$filename' is $topfolder"); + + if(!$topfolder) { + Log($name, "Unable to get top folder from '$filename'",4); + temp_cleanup($hash); + return; + } + + my $fullTarFolder="$topfolder/".FA_TAR_SUB_FOLDER; + my $depth=scalar(split("/",$fullTarFolder)); + + my $lpath=get_local_path($hash); + + my $bpath=undef; + if($lpath) { + Log($name,"Local path already exists: '$lpath'",4); + $bpath="$lpath.bak"; + + #TODO: Do not remove --> should introduce a force variant + if($bpath && -d $bpath) { + Log($name,"Trying to remove previously left backup folder '$bpath'",3); + my $res=rmtree($bpath,0,1); + } + + Log($name,"-> renaming to '$bpath'",4); + if(!rename($lpath,$bpath)) { + Log($name,"Error renaming folder '$lpath' to '$bpath'",2); + temp_cleanup($hash); + return; + } + } + + if(!$lpath || ! -d $lpath) { + if(create_fhemapp_folder($hash)) { + $lpath=get_local_path($hash); + my $cmd="tar xf $filename -C $lpath $topfolder/". FA_TAR_SUB_FOLDER . " --strip-components $depth"; + Log($name,"extract cmd '$cmd' ",4); + + my $res=system($cmd); + Log($name,"extract result: $res",4); + + } + } else { + Log($name, "Local path still exists (shouldn't): '$lpath'",2); + temp_cleanup($hash); + return; + } + + #Trying to cleanup temp-folder ... + temp_cleanup($hash); + #Updating local version (Readings) + + if($bpath && -d $bpath) { + Log($name,"Removing backup folder '$bpath' after sucessfull installlation",4); + my $res=rmtree($bpath,0,1); + } + check_local_version($hash); + + } + return; +} + + #======================================================================== +sub temp_cleanup{ +#======================================================================== + my $hash=shift // return; + my $name=$hash->{NAME}; + + if(cleanup()==0) { + Log($name,"Successfully cleaned temp folder",4); + } else { + Log($name,"Cleanup of temp folder failed!",2) + } + +} + +#======================================================================== +sub Request_Releases +#======================================================================== +{ + my $hash=shift // return; + my $name = $hash->{NAME}; + + my $continueUpdate=shift; + $continueUpdate //= 0; + + + my $url=AttrVal($name,'sourceUrl',FA_GITHUB_API_BASEURL); + $url=~ s!/*$!/!; + $url.=FA_GITHUB_API_RELEASES; + + Log($name,"Requesting: $url",4); + my $param = { + url => $url, + timeout => 5, + hash => $hash, + method => "GET", + header => "User-Agent: TeleHeater/2.2.3\r\nAccept: application/json", + callback => \&Request_Releases_Response, + continueUpdate => $continueUpdate + }; + + HttpUtils_NonblockingGet($param); + return; +} + +#======================================================================== +sub Request_Releases_Response($) +#======================================================================== +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if($err ne "") + { + Log($name,"error while requesting ".$param->{url}." - $err",5); + #readingsSingleUpdate($hash, ".fullResponse", "ERROR: $err", 0); + } + elsif($data ne "") + { + Log($name,"url ".$param->{url}." returned: $data",5); + #Log3 $name, 3, Dumper $data; + + my $rels = decode_json($data); + + my $latestPre=undef; + my $latestFull=undef; + + foreach my $rel (@{$rels}){ + Log($name,"Release: " . $rel->{tag_name},5); + my $isVer=version_compare($rel->{tag_name},FA_VERSION_LOWEST); + Log($name,"Lowest: " . FA_VERSION_LOWEST . " is: $isVer",5); + if($isVer < 0) { + next; + } + if( !$latestPre && $rel->{prerelease}) { + $latestPre=$rel; + } + if(!$latestFull && !$rel->{prerelease}) { + $latestFull=$rel; + } + } + + my $updateAvailable=0; + my $updatePath=AttrVal($name,'updatePath','stable'); + + + #Updating device readings + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash,'.latest_url',$param->{url},0); + + if($latestPre) { + Log($name,"Latest-Pre: " . $latestPre->{tag_name},4); + readingsBulkUpdateIfChanged($hash,'pre_tag_name',$latestPre->{tag_name},1); + readingsBulkUpdateIfChanged($hash,'pre_html_url',$latestPre->{html_url},1); + readingsBulkUpdateIfChanged($hash,'.pre_tarball_url',$latestPre->{tarball_url},0); + readingsBulkUpdateIfChanged($hash,'pre_info',$latestPre->{body},1); + readingsBulkUpdateIfChanged($hash,'pre_published_at',$latestPre->{published_at},1); + } else { + readingsBulkUpdateIfChanged($hash,'pre_tag_name','unknown',1); + } + if($latestFull) { + Log($name,"Latest-Stable: " . $latestFull->{tag_name},4); + readingsBulkUpdateIfChanged($hash,'stable_tag_name',$latestFull->{tag_name},1); + readingsBulkUpdateIfChanged($hash,'stable_html_url',$latestFull->{html_url},1); + readingsBulkUpdateIfChanged($hash,'.stable_tarball_url',$latestFull->{tarball_url},0); + readingsBulkUpdateIfChanged($hash,'stable_info',$latestFull->{body},1); + readingsBulkUpdateIfChanged($hash,'stable_published_at',$latestFull->{published_at},1); + } else { + readingsBulkUpdateIfChanged($hash,'stable_tag_name','unknown',1); + } + + #In case of error .... + #readingsBulkUpdate($hash, ".fullResponse", $data,0); + if($err) { + readingsBulkUpdateIfChanged($hash,'request_result','error',1); + readingsBulkUpdate($hash,'request_error',$err,1); + } else { + readingsBulkUpdateIfChanged($hash,'request_result','success',1); + } + readingsEndUpdate($hash,1); + + #Delete un-fillable version information readings + if(!$latestPre) { + readingsDelete($hash,'pre_html_url'); + readingsDelete($hash,'.pre_tarball_url'); + readingsDelete($hash,'pre_info'); + readingsDelete($hash,'pre_published_at'); + } + if(!$latestFull) { + readingsDelete($hash,'stable_html_url'); + readingsDelete($hash,'.stable_tarball_url'); + readingsDelete($hash,'stable_info'); + readingsDelete($hash,'stable_published_at'); + } + if(!$err) { + readingsDelete($hash,'request_error'); + } + } + + if($param->{continueUpdate}) { + #if called during update process ... continue with update + update($hash,1); + } + +} + + +#======================================================================== +sub set_fhemapp_link { +#======================================================================== + my $hash=shift // return; + my $name=$hash->{NAME}; + my $fa_name=$hash->{&INT_FANAME}; + + if($hash->{&INT_LOCAL_INST}) { + my $fw_me=$FW_ME; + $fw_me //= '/fhem'; + my $link="$fw_me/$fa_name/index.html#/$name"; + $hash->{&INT_LINK}="$link"; + } else { + delete($hash->{&INT_LINK}); + } +} + + +#======================================================================== +sub AttrNames{ +#======================================================================== + my $name=shift; + + my @atts; + if (!$name) { + @atts=@attrList; + } else { + @atts=split(' ',getAllAttr($name)); + } + + my @rList; + #foreach (@attrList) { + foreach (@atts) { + push @rList,(split(':', $_))[0]; + } + return @rList; +} + +#======================================================================== +sub AttrOptions_json +#======================================================================== +{ + my $name=shift // return undef; + + my @rOpts; + foreach my $att (AttrNames($name)) { + my $val=AttrVal($name,$att,undef); + push @rOpts, "\"$att\":\"$val\"" if(defined($val)); + } + my $jOpts=join(',',@rOpts); + + return $jOpts if($jOpts); + + return undef; +} + +#======================================================================== +sub get_config +#======================================================================== +{ + my $hash=shift // return; + + my $name=$hash->{NAME}; + + my $jOpts=AttrOptions_json($name); + my $config=$hash->{helper}{config}; + + if(!$config) { + $config=$config=ReadingsVal($name,".config",undef); + if($config) { + set_config($hash,$config,1); + readingsDelete($hash,'.config'); + } + } + + if($config) { + my $ret; + if($jOpts) { + $jOpts="{$jOpts}"; + ($ret = $config) =~ s{(.*)\}}{$1,"attributes":$jOpts\}}xms; + } else { + $ret=$config; + } + + return $ret if($ret); + + } + + return jsonError("No config found!"); +} + +#======================================================================== +sub set_config +#======================================================================== +{ + my $hash=shift // return; + my $newVal=shift // return; + my $writeConfig =shift; + + $writeConfig //= 0; + + my $name=$hash->{NAME}; + + $hash->{helper}{config}=$newVal; + + WriteConfig($hash) if($writeConfig); + +} + +#======================================================================== +sub get_config_file +#======================================================================== +{ + my $name=shift // return; + return lc("$FW_confdir/${name}_config.".FA_MOD_TYPE); +} + + +#======================================================================== +sub WriteConfig +#======================================================================== +{ + my $hash=shift // return; + my $name=$hash->{NAME}; + + if(InternalVal('global','configfile','x') ne 'configDB') { + if(! -d $FW_confdir) { + if(!mkdir($FW_confdir)) { + Log($name,"Unable to create missing config folder '$FW_confdir'",2); + Log($name,"Cannot write config!",2); + return; + } + } + } + + my $config=$hash->{helper}{config}; + + if($config) { + my @content; + push @content,$hash->{helper}{config}; + + my $filename=get_config_file($name); + Log($name,"Writing config '$filename'...",4); + FileWrite($filename,@content); + readingsSingleUpdate($hash,'configLastWrite',localtime(),0); + $hash->{&INT_CONFIG_FILE}=$filename if($hash->{&INT_CONFIG_FILE} ne $filename); + } else { + Log($name,"No config to write!",3); + } + return; +} + +#======================================================================== +sub ReadConfig +#======================================================================== +{ + my $hash=shift // return; + my $event=shift; + $event //=0; + $event=1 if($event ne 0); + + my $name=$hash->{NAME}; + my $filename=get_config_file($name); + + Log($name,"Reading config '$filename' ...",4); + + my ($err,@content)=FileRead($filename); + if(!$err) { + $hash->{helper}{config}=join('',@content); + readingsSingleUpdate($hash,'configLastRead',localtime(),$event); + } else { + readingsSingleUpdate($hash,'configLastRead',$err,$event); + Log($name,"ERROR: Reading config!",2); + Log($name,$err,2); + } + + return; +} + +#======================================================================== +sub DeleteConfig +#======================================================================== +{ + my $hash = shift // return; + my $name =shift; + $name //= $hash->{NAME}; + + my $filename=get_config_file($name); + Log($name,"Deleting config '$filename' ...",4); + FileDelete($filename); + + return; +} + + +#======================================================================== +sub get_local_path +#======================================================================== +{ + my $hash=shift // return undef; + my $append=shift; + + my $name=$hash->{NAME}; + my $path=InternalVal($name,INT_PATH,undef); + + + + if($path) { + if(-d $path) { + if($append) { + $path =~ s!/*$!/!; + $path .= $append; + } + return $path; + } + } + return undef; +} + +#======================================================================== +sub get_local_version +#======================================================================== +{ + my $hash=shift // return; + + + my $name=$hash->{NAME}; + my $filename=get_local_path($hash,FA_VERSION_FILENAME); + + + + if($filename) { + my ($err,@content)=FileRead({FileName => $filename,ForceType => 'file'}); + + my $config=''; + + if(FA_VERSION_FILENAME=~/\.json/) { + #handling vesion.json + if(!$err) { + $config=join('',@content); + } + my $data=decode_json($config); + return $data->{version}; + } else { + #handling CHANGELOG.md + my $vLine=shift @content; + return (split(' ',$vLine))[-2]; + } + } + return undef; +} + +#======================================================================== +sub check_local_version +#======================================================================== +{ + my $hash=shift // return; + + if($hash->{&INT_LOCAL_INST}) { + readingsBeginUpdate($hash); + + #my $ver=get_local_version($hash); + + + #--- + my $filename=get_local_path($hash,FA_VERSION_FILENAME); + + my $ver='unknown'; + if($filename) { + my ($err,@content)=FileRead({FileName => $filename,ForceType => 'file'}); + + my $config=''; + + readingsBulkUpdateIfChanged($hash,'.local_version_src',FA_VERSION_FILENAME,1); + + if(FA_VERSION_FILENAME=~/\.json/) { + #handling vesion.json + if(!$err) { + $config=join('',@content); + } + my $data=decode_json($config); + $ver=$data->{version}; + } else { + #handling CHANGELOG.md + my $vLine=shift @content; + $ver=(split(' ',$vLine))[-2]; + } + } + + readingsBulkUpdateIfChanged($hash,'local_version',$ver,1); + + readingsEndUpdate($hash,1); + + } + else + { + readingsDelete($hash,'local_version'); + readingsDelete($hash,'.local_version_src'); + } + return; +} + +#======================================================================== +sub StartLoop +#======================================================================== +{ + my $hash=shift // return; + my $time=shift; + my $stop=shift; + my $force=shift; + + my $name=$hash->{NAME}; + + my $currentInterval=AttrVal($name,'interval',FA_DEFAULT_INTERVAL); + if($currentInterval < FA_DEFAULT_INTERVAL) { + StopLoop($hash); + Log($name,"Interval is lower than " . FA_DEFAULT_INTERVAL . " seconds. Stopping all loop activities",5); + return; + } + if(!IsDisabled($name) or $force) { + $stop //= 1; + $time //= 0; + $time = AttrVal($name,'interval',FA_DEFAULT_INTERVAL) if(!(0+$time)); + StopLoop($hash) if($stop); + Log($name,"Starting internal timer loop ($time sec.)",5); + my $nextTimer=gettimeofday()+$time; + InternalTimer($nextTimer,\&Loop,$hash); + readingsSingleUpdate($hash,'next_cycle',localtime($nextTimer),0); + } + + return; +} + +#======================================================================== + sub Loop +#======================================================================== +{ + my $hash = shift // return; + + my $name = $hash->{NAME}; + Log($name,"Internal timer loop elapsed",5); + + check_local_version($hash); + Request_Releases($hash); + + StartLoop($hash); + + return; +} + +#======================================================================== +sub StopLoop +#======================================================================== +{ + my $hash=shift // return; + my $name=$hash->{NAME}; + + Log($name,"Stopping internal timer loop",5); + RemoveInternalTimer($hash); + readingsSingleUpdate($hash,'next_cycle','disabled',0); + + return; +} + +############################################################################ +# HELPER - Functions +############################################################################ + + #======================================================================== + sub Log + #======================================================================== + #own log routine adding package and device name to log entries + { + my $name=shift; + my $msg=shift; + my $verb=shift; + + $msg //= 'noMsg'; + my $verbRef = $name; #(undef=global!) + $name //= __PACKAGE__; + $verb //= 3; + + + Log3 $verbRef,$verb,'['.$name.']: '.$msg; + return undef + } + + #======================================================================== + sub version_compare + #======================================================================== + { + #returns 1 if v1 > v2 + #returns -1 if v1 < v1 + #returns 0 if v1 = v2 + #if number of version parts are different, only minimum number of + #version parts are compared. So '1.4' = '1.4.2' and '1.4' = '1.4.8' ... + my $v1=shift // return; + my $v2=shift // return; + + $v1=$v1 =~ s/[^0-9.]//rg; + $v1=$v1 =~ s/^\.//gr; + + $v2=$v2 =~ s/[^0-9.]//rg; + $v2=$v2 =~ s/^\.//gr; + + my @sv1= split /\./, $v1; + my @sv2= split /\./, $v2; + + return if($#sv1 < 0 || $#sv2 < 0); + + my $min=$#sv1; + if($#sv2 < $min) { + $min=$#sv2 + } + + my $result=0; + + for(my $i=0;$i<=$min;$i++) { + if($sv1[$i]+0 > $sv2[$i]+0) { + $result=1; + last; + } elsif($sv1[$i]+0 < $sv2[$i]+0) { + $result=-1; + last; + } + } + + return $result; + + } + + #======================================================================== + sub jsonError { + #======================================================================== + my $err=shift; + return "{\"error\":\"$err\"}"; + } + + + + +######################################################################### +# HELP - Documentation for FHEM help command in EN and DE +#------------------------------------------------------------------------ +# must be validated with ./contrib/commandref_join.pl -> No Errors! +######################################################################### +1; + +=pod +=item helper +=item summary Settings and special functions for FHEMapp-UI +=item summary_DE Einstellungen und Spezialfunktionalitaet fuer das FHEMapp-UI + +=begin html + + +

fhemApp

+ + +=end html + +=begin html_DE + + +

FHEMAPP

+ + +=end html_DE + +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index db09641f0..9a765c0b3 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -50,6 +50,7 @@ FHEM/00_TUL.pm andi291 KNX/EIB FHEM/00_ZWCUL.pm rudolfkoenig ZWave FHEM/00_ZWDongle.pm rudolfkoenig ZWave FHEM/01_FHEMWEB.pm rudolfkoenig Frontends/FHEMWEB +FHEM/02_FHEMAPP.pm Benni Frontends/FHEMapp FHEM/02_FRAMEBUFFER.pm kaihs Frontends FHEM/02_FTUISRV.pm viegener Frontends/TabletUI FHEM/02_HTTPAPI.pm klaus.schauer Automatisierung