diff --git a/fhem/FHEM/10_RESIDENTS.pm b/fhem/FHEM/10_RESIDENTS.pm index 8b22f338f..e827ac7bd 100644 --- a/fhem/FHEM/10_RESIDENTS.pm +++ b/fhem/FHEM/10_RESIDENTS.pm @@ -26,7 +26,7 @@ sub RESIDENTS_Initialize($) { . "rgr_states:multiple-strict,home,gotosleep,asleep,awoken,absent,gone rgr_lang:EN,DE rgr_noDuration:0,1 rgr_showAllStates:0,1 rgr_wakeupDevice " . $readingFnAttributes; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # module Fn #################################################################### diff --git a/fhem/FHEM/20_GUEST.pm b/fhem/FHEM/20_GUEST.pm index 0cfe568ee..6a5903c99 100644 --- a/fhem/FHEM/20_GUEST.pm +++ b/fhem/FHEM/20_GUEST.pm @@ -31,7 +31,7 @@ sub GUEST_Initialize($) { $hash->{AttrList} .= " " . $hash->{AttrPrefix} . $_; } - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } 1; diff --git a/fhem/FHEM/20_ROOMMATE.pm b/fhem/FHEM/20_ROOMMATE.pm index a7e42887b..bcd57e165 100644 --- a/fhem/FHEM/20_ROOMMATE.pm +++ b/fhem/FHEM/20_ROOMMATE.pm @@ -31,7 +31,7 @@ sub ROOMMATE_Initialize($) { $hash->{AttrList} .= " " . $hash->{AttrPrefix} . $_; } - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } 1; diff --git a/fhem/FHEM/42_npmjs.pm b/fhem/FHEM/42_npmjs.pm index a76aa94cf..7ad6ac73b 100644 --- a/fhem/FHEM/42_npmjs.pm +++ b/fhem/FHEM/42_npmjs.pm @@ -21,7 +21,7 @@ sub npmjs_Initialize($) { . "npmglobal:1,0 " . $readingFnAttributes; - return FHEM::Meta::Load( __FILE__, $modHash ); + return FHEM::Meta::InitMod( __FILE__, $modHash ); } # define package @@ -99,7 +99,7 @@ sub Define($$) { $attr{$name}{alias} = 'Node.js Package Update Status'; $attr{$name}{devStateIcon} = 'npm.updates.available:security@red:outdated npm.is.up.to.date:security@green:outdated .*npm.outdated.*in.progress:system_fhem_reboot@orange .*in.progress:system_fhem_update@orange warning.*:message_attention@orange error.*:message_attention@red'; - $attr{$name}{group} = 'System'; + $attr{$name}{group} = 'Update'; $attr{$name}{icon} = 'npm-old'; $attr{$name}{room} = 'System'; } @@ -408,7 +408,7 @@ sub Set($$@) { # return Usage: else { - my $list = ""; + my $list = ''; if ( !defined( $hash->{".fhem"}{npm}{nodejsversions} ) ) { $list = @@ -574,7 +574,7 @@ sub Get($$@) { return $ret; } else { - my $list = ""; + my $list = ''; $list .= " showOutdatedList:noArg" if ( defined( $hash->{".fhem"}{npm}{outdatedpackages} ) and scalar keys %{ $hash->{".fhem"}{npm}{outdatedpackages} } > 0 ); @@ -638,7 +638,7 @@ sub DoModuleTrigger($$@) { $noreplace = 1 unless ( defined($noreplace) ); $TYPE = $hash->{TYPE} unless ( defined($TYPE) ); - return "" + return '' unless ( defined($TYPE) && defined( $modules{$TYPE} ) && defined($eventString) @@ -660,7 +660,7 @@ sub DoModuleTrigger($$@) { # This is a global event on module level and in device context return "$event: missing device name" - if ( !defined($dev) || $dev eq "" ); + if ( !defined($dev) || $dev eq '' ); return DoTrigger( "global", "$TYPE:$eventString", $noreplace ); } @@ -803,13 +803,13 @@ sub ExecuteNpmCommand($) { my $npm = {}; $npm->{debug} = $cmd->{debug}; - my $cmdPrefix = ""; - my $cmdSuffix = ""; + my $cmdPrefix = ''; + my $cmdSuffix = ''; if ( $cmd->{host} =~ /^(?:(.*)@)?([^:]+)(?::(\d+))?$/ && lc($2) ne "localhost" ) { - my $port = ""; + my $port = ''; if ($3) { $port = "-p $3 "; } @@ -830,7 +830,7 @@ sub ExecuteNpmCommand($) { # wrap SSH command $cmdPrefix .= - 'ssh -oBatchMode=yes ' . $port . ( $1 ? "$1@" : "" ) . $2 . ' \''; + 'ssh -oBatchMode=yes ' . $port . ( $1 ? "$1@" : '' ) . $2 . ' \''; $cmdSuffix = '\' 2>&1'; } @@ -907,7 +907,7 @@ sub ExecuteNpmCommand($) { } } else { - my @packages = ""; + my @packages = ''; foreach my $package ( split / /, $1 ) { next unless ( $package =~ @@ -929,14 +929,14 @@ sub ExecuteNpmCommand($) { push @packages, $package; } my $pkglist = join( ' ', @packages ); - return unless ( $pkglist ne "" ); + return unless ( $pkglist ne '' ); $npm->{npminstall} =~ s/%PACKAGES%/$pkglist/gi; } print qq($npm->{npminstall}\n) if ( $npm->{debug} == 1 ); $response = NpmInstall($npm); } elsif ( $cmd->{cmd} =~ /^uninstall (.+)/ ) { - my @packages = ""; + my @packages = ''; foreach my $package ( split / /, $1 ) { next unless ( $package =~ @@ -944,13 +944,13 @@ sub ExecuteNpmCommand($) { push @packages, $package; } my $pkglist = join( ' ', @packages ); - return unless ( $pkglist ne "" ); + return unless ( $pkglist ne '' ); $npm->{npmuninstall} =~ s/%PACKAGES%/$pkglist/gi; print qq($npm->{npmuninstall}\n) if ( $npm->{debug} == 1 ); $response = NpmUninstall($npm); } elsif ( $cmd->{cmd} =~ /^update(?: (.+))?/ ) { - my $pkglist = ""; + my $pkglist = ''; if ( defined($1) ) { my @packages; foreach my $package ( split / /, $1 ) { @@ -1029,7 +1029,7 @@ sub RetrieveNpmOutput($$) { my $p = shift; my $h = {}; - return $h unless ( defined($p) && $p ne "" ); + return $h unless ( defined($p) && $p ne '' ); # first try to interprete text as JSON directly my $decode_json = eval { decode_json($p) }; @@ -1344,21 +1344,21 @@ sub CreateInstalledList($$) { my $html = defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0; $packages = $hash->{".fhem"}{npm}{listedpackages}{dependencies}; - my $header = ""; - my $footer = ""; + my $header = ''; + my $footer = ''; if ($html) { $header = ''; $footer = '
'; } - my $rowOpen = ""; - my $rowOpenEven = ""; - my $rowOpenOdd = ""; - my $colOpen = ""; - my $txtOpen = ""; - my $txtClose = ""; + my $rowOpen = ''; + my $rowOpenEven = ''; + my $rowOpenOdd = ''; + my $colOpen = ''; + my $txtOpen = ''; + my $txtClose = ''; my $colClose = "\t\t\t"; - my $rowClose = ""; + my $rowClose = ''; if ($html) { $rowOpen = ''; @@ -1417,21 +1417,21 @@ sub CreateOutdatedList($$) { $packages = $hash->{".fhem"}{npm}{outdatedpackages}; my $npmglobal = ( AttrVal( $hash->{NAME}, 'npmglobal', 1 ) eq '1' ? 1 : 0 ); - my $header = ""; - my $footer = ""; + my $header = ''; + my $footer = ''; if ($html) { $header = ''; $footer = '
'; } - my $rowOpen = ""; - my $rowOpenEven = ""; - my $rowOpenOdd = ""; - my $colOpen = ""; - my $txtOpen = ""; - my $txtClose = ""; + my $rowOpen = ''; + my $rowOpenEven = ''; + my $rowOpenOdd = ''; + my $colOpen = ''; + my $txtOpen = ''; + my $txtClose = ''; my $colClose = "\t\t\t"; - my $rowClose = ""; + my $rowClose = ''; if ($html) { $rowOpen = ''; @@ -1748,7 +1748,7 @@ sub ToDay() { "node", "npm" ], - "version": "v1.0.4", + "version": "v1.0.5", "release_status": "stable", "author": [ "Julian Pawlowski " @@ -1762,7 +1762,7 @@ sub ToDay() { "prereqs": { "runtime": { "requires": { - "FHEM": 5.00918623, + "FHEM": 5.00918799, "perl": 5.014, "GPUtils": 0, "JSON": 0, @@ -1881,20 +1881,9 @@ sub ToDay() { } }, "resources": { - "license": [ - "https://fhem.de/#License" - ], - "homepage": "https://fhem.de/", "bugtracker": { "web": "https://forum.fhem.de/index.php/board,29.0.html", - "x_web_title": "Sonstige Systeme" - }, - "repository": { - "type": "svn", - "url": "https://svn.fhem.de/fhem/", - "x_branch_master": "trunk", - "x_branch_dev": "trunk", - "web": "https://svn.fhem.de/" + "x_web_title": "FHEM Forum: Sonstige Systeme" } } } diff --git a/fhem/FHEM/50_HP1000.pm b/fhem/FHEM/50_HP1000.pm index 6fe5ee28d..8da9468db 100755 --- a/fhem/FHEM/50_HP1000.pm +++ b/fhem/FHEM/50_HP1000.pm @@ -268,7 +268,7 @@ sub HP1000_Initialize($) { }, }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/59_Wunderground.pm b/fhem/FHEM/59_Wunderground.pm index 07bf19dda..e553b1ca1 100644 --- a/fhem/FHEM/59_Wunderground.pm +++ b/fhem/FHEM/59_Wunderground.pm @@ -295,7 +295,7 @@ sub Wunderground_Initialize($) { 'wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' } }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/70_ENIGMA2.pm b/fhem/FHEM/70_ENIGMA2.pm index 45efe2986..fcaf3a02c 100644 --- a/fhem/FHEM/70_ENIGMA2.pm +++ b/fhem/FHEM/70_ENIGMA2.pm @@ -60,7 +60,7 @@ sub ENIGMA2_Initialize($) { }, }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/70_LaMetric2.pm b/fhem/FHEM/70_LaMetric2.pm index a0d31a950..e159dcf27 100644 --- a/fhem/FHEM/70_LaMetric2.pm +++ b/fhem/FHEM/70_LaMetric2.pm @@ -249,7 +249,7 @@ sub LaMetric2_Initialize($$) { #$hash->{parseParams} = 1; # not possible due to legacy msg command schema $hash->{'.msgParams'} = { parseParams => 1, }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } #------------------------------------------------------------------------------ diff --git a/fhem/FHEM/70_PHTV.pm b/fhem/FHEM/70_PHTV.pm index 4de3e997e..b0b5a5132 100644 --- a/fhem/FHEM/70_PHTV.pm +++ b/fhem/FHEM/70_PHTV.pm @@ -50,7 +50,7 @@ sub PHTV_Initialize($) { }; FHEM_colorpickerInit(); - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/70_Pushover.pm b/fhem/FHEM/70_Pushover.pm index b6df3ad0d..5a51e6a8d 100644 --- a/fhem/FHEM/70_Pushover.pm +++ b/fhem/FHEM/70_Pushover.pm @@ -25,7 +25,7 @@ sub Pushover_Initialize($$) { #$hash->{parseParams} = 1; # not possible due to legacy msg command schema $hash->{'.msgParams'} = { parseParams => 1, }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/74_THINKINGCLEANER.pm b/fhem/FHEM/74_THINKINGCLEANER.pm index 2cb0df5b9..56a138665 100644 --- a/fhem/FHEM/74_THINKINGCLEANER.pm +++ b/fhem/FHEM/74_THINKINGCLEANER.pm @@ -58,7 +58,7 @@ sub THINKINGCLEANER_Initialize($) { }, }; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/75_msgConfig.pm b/fhem/FHEM/75_msgConfig.pm index 5df7f65dc..0359d5174 100755 --- a/fhem/FHEM/75_msgConfig.pm +++ b/fhem/FHEM/75_msgConfig.pm @@ -146,7 +146,7 @@ sub msgConfig_Initialize($) { addToAttrList($_); } - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/98_GEOFANCY.pm b/fhem/FHEM/98_GEOFANCY.pm index c205150a6..da5c638f5 100755 --- a/fhem/FHEM/98_GEOFANCY.pm +++ b/fhem/FHEM/98_GEOFANCY.pm @@ -18,7 +18,7 @@ sub GEOFANCY_Initialize($) { $hash->{SetFn} = "GEOFANCY_Set"; $hash->{AttrList} = "devAlias disable:0,1 " . $readingFnAttributes; - return FHEM::Meta::Load( __FILE__, $hash ); + return FHEM::Meta::InitMod( __FILE__, $hash ); } # regular Fn ################################################################## diff --git a/fhem/FHEM/98_Installer.pm b/fhem/FHEM/98_Installer.pm new file mode 100644 index 000000000..be46d2ff3 --- /dev/null +++ b/fhem/FHEM/98_Installer.pm @@ -0,0 +1,1647 @@ +# $Id$ + +package main; +use strict; +use warnings; +use FHEM::Meta; + +sub Installer_Initialize($) { + my ($modHash) = @_; + + # $modHash->{SetFn} = "FHEM::Installer::Set"; + $modHash->{GetFn} = "FHEM::Installer::Get"; + $modHash->{DefFn} = "FHEM::Installer::Define"; + $modHash->{NotifyFn} = "FHEM::Installer::Notify"; + $modHash->{UndefFn} = "FHEM::Installer::Undef"; + $modHash->{AttrFn} = "FHEM::Installer::Attr"; + $modHash->{AttrList} = + "disable:1,0 " + . "disabledForIntervals " + . "updateListReading:1,0 " + . $readingFnAttributes; + + return FHEM::Meta::InitMod( __FILE__, $modHash ); +} + +# define package +package FHEM::Installer; +use strict; +use warnings; +use POSIX; +use FHEM::Meta; + +use GPUtils qw(GP_Import); +use JSON; +use Data::Dumper; + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw(readingsSingleUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsBeginUpdate + readingsEndUpdate + ReadingsTimestamp + defs + modules + Log + Log3 + Debug + DoTrigger + CommandAttr + attr + AttrVal + ReadingsVal + Value + IsDisabled + deviceEvents + init_done + gettimeofday + InternalTimer + RemoveInternalTimer) + ); +} + +our $coreUpdate; +our %corePackageUpdates; +our %coreFileUpdates; + +our %moduleUpdates; +our %packageUpdates; +our %fileUpdates; + +sub Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + # Initialize the module and the device + return $@ unless ( FHEM::Meta::SetInternals($hash) ); + use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + + my $name = $a[0]; + my $host = $a[2] ? $a[2] : 'localhost'; + + Undef( $hash, undef ) if ( $hash->{OLDDEF} ); # modify + + $hash->{NOTIFYDEV} = "global,$name"; + + return "Existing instance: " + . $modules{ $hash->{TYPE} }{defptr}{localhost}{NAME} + if ( defined( $modules{ $hash->{TYPE} }{defptr}{localhost} ) ); + + $modules{ $hash->{TYPE} }{defptr}{localhost} = $hash; + + if ( $init_done && !defined( $hash->{OLDDEF} ) ) { + + # presets for FHEMWEB + $attr{$name}{alias} = 'FHEM Installer Status'; + $attr{$name}{devStateIcon} = +'fhem.updates.available:security@red:outdated fhem.is.up.to.date:security@green:outdated .*fhem.outdated.*in.progress:system_fhem_reboot@orange .*in.progress:system_fhem_update@orange warning.*:message_attention@orange error.*:message_attention@red'; + $attr{$name}{group} = 'Update'; + $attr{$name}{icon} = 'system_fhem'; + $attr{$name}{room} = 'System'; + } + + # __GetUpdatedata() unless ( defined($coreUpdate) ); + + readingsSingleUpdate( $hash, "state", "initialized", 1 ) + if ( ReadingsVal( $name, 'state', 'none' ) ne 'none' ); + + return undef; +} + +sub Undef($$) { + + my ( $hash, $arg ) = @_; + + my $name = $hash->{NAME}; + + if ( exists( $hash->{".fhem"}{subprocess} ) ) { + my $subprocess = $hash->{".fhem"}{subprocess}; + $subprocess->terminate(); + $subprocess->wait(); + } + + RemoveInternalTimer($hash); + + delete( $modules{installer}{defptr}{ $hash->{HOST} } ); + return undef; +} + +sub Attr(@) { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + + if ( $attrName eq "disable" ) { + if ( $cmd eq "set" and $attrVal eq "1" ) { + RemoveInternalTimer($hash); + + readingsSingleUpdate( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "Installer ($name) - disabled"; + } + + elsif ( $cmd eq "del" ) { + Log3 $name, 3, "Installer ($name) - enabled"; + } + } + + elsif ( $attrName eq "disabledForIntervals" ) { + if ( $cmd eq "set" ) { + return +"check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'" + unless ( $attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/ ); + Log3 $name, 3, "Installer ($name) - disabledForIntervals"; + readingsSingleUpdate( $hash, "state", "disabled", 1 ); + } + + elsif ( $cmd eq "del" ) { + Log3 $name, 3, "Installer ($name) - enabled"; + readingsSingleUpdate( $hash, "state", "active", 1 ); + } + } + + return undef; +} + +sub Notify($$) { + + my ( $hash, $dev ) = @_; + my $name = $hash->{NAME}; + return if ( IsDisabled($name) ); + + my $devname = $dev->{NAME}; + my $devtype = $dev->{TYPE}; + my $events = deviceEvents( $dev, 1 ); + return if ( !$events ); + + Log3 $name, 5, "Installer ($name) - Notify: " . Dumper $events; + + if ( + ( + ( + grep ( /^DEFINED.$name$/, @{$events} ) + or grep ( /^DELETEATTR.$name.disable$/, @{$events} ) + or grep ( /^ATTR.$name.disable.0$/, @{$events} ) + ) + and $devname eq 'global' + and $init_done + ) + or ( + ( + grep ( /^INITIALIZED$/, @{$events} ) + or grep ( /^REREADCFG$/, @{$events} ) + or grep ( /^MODIFIED.$name$/, @{$events} ) + ) + and $devname eq 'global' + ) + ) + { + # Load metadata for all modules that are in use + FHEM::Meta::Load(); + } + + if ( + $devname eq $name + and ( grep ( /^installed:.successful$/, @{$events} ) + or grep ( /^uninstalled:.successful$/, @{$events} ) + or grep ( /^updated:.successful$/, @{$events} ) ) + ) + { + $hash->{".fhem"}{installer}{cmd} = 'outdated'; + AsynchronousExecuteFhemCommand($hash); + } + + return; +} + +sub Get($$@) { + + my ( $hash, $name, @aa ) = @_; + + my ( $cmd, @args ) = @aa; + + if ( $cmd eq 'showModuleInfo' ) { + return "usage: $cmd " if ( @args != 1 ); + + my $ret = CreateMetadataList( $hash, $cmd, $args[0] ); + return $ret; + } + else { + my $fhemModules; + + foreach ( sort keys %modules ) { + next if ( $_ eq 'Global' ); + $fhemModules .= ',' if ($fhemModules); + $fhemModules .= $_; + } + + my $list = "showModuleInfo:FHEM,$fhemModules"; + + return "Unknown argument $cmd, choose one of $list"; + } +} + +sub Event ($$) { + my $hash = shift; + my $event = shift; + my $name = $hash->{NAME}; + + return + unless ( defined( $hash->{".fhem"}{installer}{cmd} ) + && $hash->{".fhem"}{installer}{cmd} =~ + m/^(install|uninstall|update)(?: (.+))/i ); + + my $cmd = $1; + my $packages = $2; + + my $list; + + foreach my $package ( split / /, $packages ) { + next + unless ( + $package =~ /^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ ); + $list .= " " if ($list); + $list .= $2; + } + + DoModuleTrigger( $hash, uc($event) . uc($cmd) . " $name $list" ); +} + +sub DoModuleTrigger($$@) { + my ( $hash, $eventString, $noreplace, $TYPE ) = @_; + $hash = $defs{$hash} unless ( ref($hash) ); + $noreplace = 1 unless ( defined($noreplace) ); + $TYPE = $hash->{TYPE} unless ( defined($TYPE) ); + + return '' + unless ( defined($TYPE) + && defined( $modules{$TYPE} ) + && defined($eventString) + && $eventString =~ + m/^([A-Za-z\d._]+)(?:\s+([A-Za-z\d._]+)(?:\s+(.+))?)?$/ ); + + my $event = $1; + my $dev = $2; + + return "DoModuleTrigger() can only handle module related events" + if ( ( $hash->{NAME} && $hash->{NAME} eq "global" ) + || $dev eq "global" ); + + # This is a global event on module level + return DoTrigger( "global", "$TYPE:$eventString", $noreplace ) + unless ( $event =~ +/^INITIALIZED|INITIALIZING|MODIFIED|DELETED|BEGIN(?:UPDATE|INSTALL|UNINSTALL)|END(?:UPDATE|INSTALL|UNINSTALL)$/ + ); + + # This is a global event on module level and in device context + return "$event: missing device name" + if ( !defined($dev) || $dev eq '' ); + + return DoTrigger( "global", "$TYPE:$eventString", $noreplace ); +} + +################################### +sub ProcessUpdateTimer($) { + my $hash = shift; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash); + InternalTimer( + gettimeofday() + 14400, + "FHEM::Installer::ProcessUpdateTimer", + $hash, 0 + ); + Log3 $name, 4, "Installer ($name) - stateRequestTimer: Call Request Timer"; + + unless ( IsDisabled($name) ) { + if ( exists( $hash->{".fhem"}{subprocess} ) ) { + Log3 $name, 2, + "Installer ($name) - update in progress, process aborted."; + return 0; + } + + readingsSingleUpdate( $hash, "state", "ready", 1 ) + if ( ReadingsVal( $name, 'state', 'none' ) eq 'none' + or ReadingsVal( $name, 'state', 'none' ) eq 'initialized' ); + + if ( + ToDay() ne ( + split( + ' ', ReadingsTimestamp( $name, 'outdated', '1970-01-01' ) + ) + )[0] + or ReadingsVal( $name, 'state', '' ) eq 'disabled' + ) + { + $hash->{".fhem"}{installer}{cmd} = 'outdated'; + AsynchronousExecuteFhemCommand($hash); + } + } +} + +sub CleanSubprocess($) { + + my $hash = shift; + + my $name = $hash->{NAME}; + + delete( $hash->{".fhem"}{subprocess} ); + Log3 $name, 4, "Installer ($name) - clean Subprocess"; +} + +use constant POLLINTERVAL => 1; + +sub AsynchronousExecuteFhemCommand($) { + + require "SubProcess.pm"; + my ($hash) = shift; + + my $name = $hash->{NAME}; + + my $subprocess = SubProcess->new( { onRun => \&OnRun } ); + $subprocess->{installer} = $hash->{".fhem"}{installer}; + $subprocess->{installer}{host} = $hash->{HOST}; + $subprocess->{installer}{debug} = + ( AttrVal( $name, 'verbose', 0 ) > 3 ? 1 : 0 ); + my $pid = $subprocess->run(); + + readingsSingleUpdate( + $hash, + 'state', + 'command \'fhem ' . $hash->{".fhem"}{installer}{cmd} . '\' in progress', + 1 + ); + + if ( !defined($pid) ) { + Log3 $name, 1, + "Installer ($name) - Cannot execute command asynchronously"; + + CleanSubprocess($hash); + readingsSingleUpdate( $hash, 'state', + 'Cannot execute command asynchronously', 1 ); + return undef; + } + + Event( $hash, "BEGIN" ); + Log3 $name, 4, + "Installer ($name) - execute command asynchronously (PID= $pid)"; + + $hash->{".fhem"}{subprocess} = $subprocess; + + InternalTimer( gettimeofday() + POLLINTERVAL, + "FHEM::Installer::PollChild", $hash, 0 ); + Log3 $hash, 4, "Installer ($name) - control passed back to main loop."; +} + +sub PollChild($) { + + my $hash = shift; + + my $name = $hash->{NAME}; + my $subprocess = $hash->{".fhem"}{subprocess}; + my $json = $subprocess->readFromChild(); + + if ( !defined($json) ) { + Log3 $name, 5, + "Installer ($name) - still waiting (" + . $subprocess->{lasterror} . ")."; + InternalTimer( gettimeofday() + POLLINTERVAL, + "FHEM::Installer::PollChild", $hash, 0 ); + return; + } + else { + Log3 $name, 4, + "Installer ($name) - got result from asynchronous parsing."; + $subprocess->wait(); + Log3 $name, 4, "Installer ($name) - asynchronous finished."; + + CleanSubprocess($hash); + PreProcessing( $hash, $json ); + } +} + +###################################### +# Begin Childprocess +###################################### + +sub OnRun() { + my $subprocess = shift; + my $response = ExecuteFhemCommand( $subprocess->{installer} ); + + my $json = eval { encode_json($response) }; + if ($@) { + Log3 'Installer OnRun', 3, "Installer - JSON error: $@"; + $json = "{\"jsonerror\":\"$@\"}"; + } + + $subprocess->writeToParent($json); +} + +sub ExecuteFhemCommand($) { + + my $cmd = shift; + + my $installer = {}; + $installer->{debug} = $cmd->{debug}; + + my $cmdPrefix = ''; + my $cmdSuffix = ''; + + if ( $cmd->{host} =~ /^(?:(.*)@)?([^:]+)(?::(\d+))?$/ + && lc($2) ne "localhost" ) + { + my $port = ''; + if ($3) { + $port = "-p $3 "; + } + + # One-time action to add remote hosts key. + # If key changes, user will need to intervene + # and cleanup known_hosts file manually for security reasons + $cmdPrefix = + 'KEY=$(ssh-keyscan -t ed25519 ' + . $2 + . ' 2>/dev/null); ' + . 'grep -q -E "^${KEY% *}" ${HOME}/.ssh/known_hosts || echo "${KEY}" >> ${HOME}/.ssh/known_hosts; '; + $cmdPrefix .= + 'KEY=$(ssh-keyscan -t rsa ' + . $2 + . ' 2>/dev/null); ' + . 'grep -q -E "^${KEY% *}" ${HOME}/.ssh/known_hosts || echo "${KEY}" >> ${HOME}/.ssh/known_hosts; '; + + # wrap SSH command + $cmdPrefix .= + 'ssh -oBatchMode=yes ' . $port . ( $1 ? "$1@" : '' ) . $2 . ' \''; + $cmdSuffix = '\' 2>&1'; + } + + my $global = '-g '; + my $sudo = 'sudo -n '; + + if ( $cmd->{npmglobal} eq '0' ) { + $global = ''; + $sudo = ''; + } + + $installer->{npminstall} = + $cmdPrefix + . 'echo n | sh -c "' + . $sudo + . 'NODE_ENV=${NODE_ENV:-production} npm install ' + . $global + . '--json --silent --unsafe-perm %PACKAGES%" 2>&1' + . $cmdSuffix; + $installer->{npmuninstall} = + $cmdPrefix + . 'echo n | sh -c "' + . $sudo + . 'NODE_ENV=${NODE_ENV:-production} npm uninstall ' + . $global + . '--json --silent %PACKAGES%" 2>&1' + . $cmdSuffix; + $installer->{npmupdate} = + $cmdPrefix + . 'echo n | sh -c "' + . $sudo + . 'NODE_ENV=${NODE_ENV:-production} npm update ' + . $global + . '--json --silent --unsafe-perm %PACKAGES%" 2>&1' + . $cmdSuffix; + $installer->{npmoutdated} = + $cmdPrefix + . 'echo n | ' + . 'echo "{' . "\n" + . '\"versions\": "; ' + . 'node -e "console.log(JSON.stringify(process.versions));"; ' + . 'L1=$(npm list ' + . $global + . '--json --silent --depth=0 2>/dev/null); ' + . '[ "$L1" != "" ] && [ "$L1" != "\n" ] && echo ", \"listed\": $L1"; ' + . 'L2=$(npm outdated ' + . $global + . '--json --silent 2>&1); ' + . '[ "$L2" != "" ] && [ "$L2" != "\n" ] && echo ", \"outdated\": $L2"; ' + . 'echo "}"' + . $cmdSuffix; + + my $response; + + if ( $cmd->{cmd} =~ /^install (.+)/ ) { + my @packages = ''; + foreach my $package ( split / /, $1 ) { + next + unless ( $package =~ + /^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ ); + + push @packages, + "homebridge" + if ( + $package =~ m/^homebridge-/i + && ( + defined( $cmd->{listedpackages} ) + and defined( $cmd->{listedpackages}{dependencies} ) + and !defined( + $cmd->{listedpackages}{dependencies}{homebridge} + ) + ) + ); + + push @packages, $package; + } + my $pkglist = join( ' ', @packages ); + return unless ( $pkglist ne '' ); + $installer->{npminstall} =~ s/%PACKAGES%/$pkglist/gi; + + print qq($installer->{npminstall}\n) if ( $installer->{debug} == 1 ); + $response = InstallerInstall($installer); + } + elsif ( $cmd->{cmd} =~ /^uninstall (.+)/ ) { + my @packages = ''; + foreach my $package ( split / /, $1 ) { + next + unless ( $package =~ + /^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ ); + push @packages, $package; + } + my $pkglist = join( ' ', @packages ); + return unless ( $pkglist ne '' ); + $installer->{npmuninstall} =~ s/%PACKAGES%/$pkglist/gi; + print qq($installer->{npmuninstall}\n) if ( $installer->{debug} == 1 ); + $response = InstallerUninstall($installer); + } + elsif ( $cmd->{cmd} =~ /^update(?: (.+))?/ ) { + my $pkglist = ''; + if ( defined($1) ) { + my @packages; + foreach my $package ( split / /, $1 ) { + next + unless ( $package =~ + /^(?:@([\w-]+)\/)?([\w-]+)(?:@([\d\.=<>]+|latest))?$/ ); + push @packages, $package; + } + $pkglist = join( ' ', @packages ); + } + $installer->{npmupdate} =~ s/%PACKAGES%/$pkglist/gi; + print qq($installer->{npmupdate}\n) if ( $installer->{debug} == 1 ); + $response = InstallerUpdate($installer); + } + elsif ( $cmd->{cmd} eq 'outdated' ) { + print qq($installer->{npmoutdated}\n) if ( $installer->{debug} == 1 ); + $response = InstallerOutdated($installer); + } + + return $response; +} + +sub InstallerUpdate($) { + my $cmd = shift; + my $p = `$cmd->{npmupdate}`; + my $ret = RetrieveInstallerOutput( $cmd, $p ); + + return $ret; +} + +sub InstallerOutdated($) { + my $cmd = shift; + my $p = `$cmd->{npmoutdated}`; + my $ret = RetrieveInstallerOutput( $cmd, $p ); + + return $ret; +} + +sub RetrieveInstallerOutput($$) { + my $cmd = shift; + my $p = shift; + my $h = {}; + + return $h unless ( defined($p) && $p ne '' ); + + # first try to interprete text as JSON directly + my $decode_json = eval { decode_json($p) }; + if ( not $@ ) { + $h = $decode_json; + } + + # if this was not successful, + # we'll disassamble the text + else { + my $o; + my $json; + my $skip = 0; + + foreach my $line ( split /\n/, $p ) { + chomp($line); + print qq($line\n) if ( $cmd->{debug} == 1 ); + + # JSON output + if ($skip) { + $json .= $line; + } + + # reached JSON + elsif ( $line =~ /^\{$/ ) { + $json = $line; + $skip = 1; + } + + # other output before JSON + else { + $o .= $line; + } + } + + $decode_json = eval { decode_json($json) }; + + # Found valid JSON output + if ( not $@ ) { + $h = $decode_json; + } + + # Final parsing error + else { + if ($o) { + if ( $o =~ m/Permission.denied.\(publickey\)\.?\r?\n?$/i ) { + $h->{error}{code} = "E403"; + $h->{error}{summary} = + "Forbidden - None of the SSH keys from ~/.ssh/ " + . "were authorized to access remote host"; + $h->{error}{detail} = $o; + } + elsif ( $o =~ + m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?not.found$/i + or $o =~ +m/(?:(\w+?): )?(?:(\w+? \d+): )?(\w+?): [^:]*?No.such.file.or.directory$/i + ) + { + $h->{error}{code} = "E404"; + $h->{error}{summary} = "Not Found - $3 is not installed"; + $h->{error}{detail} = $o; + } + else { + $h->{error}{code} = "E501"; + $h->{error}{summary} = "Parsing error - " . $@; + $h->{error}{detail} = $p; + } + } + else { + $h->{error}{code} = "E500"; + $h->{error}{summary} = "Parsing error - " . $@; + $h->{error}{detail} = $p; + } + } + } + + return $h; +} + +#################################################### +# End Childprocess +#################################################### + +sub PreProcessing($$) { + + my ( $hash, $json ) = @_; + + my $name = $hash->{NAME}; + + my $decode_json = eval { decode_json($json) }; + if ($@) { + Log3 $name, 2, "Installer ($name) - JSON error: $@"; + return; + } + + Log3 $hash, 4, "Installer ($name) - JSON: $json"; + + # safe result in hidden reading + # to restore module state after reboot + if ( $hash->{".fhem"}{installer}{cmd} eq 'outdated' ) { + delete $hash->{".fhem"}{installer}{outdatedpackages}; + $hash->{".fhem"}{installer}{outdatedpackages} = $decode_json->{outdated} + if ( defined( $decode_json->{outdated} ) ); + delete $hash->{".fhem"}{installer}{listedpackages}; + $hash->{".fhem"}{installer}{listedpackages} = $decode_json->{listed} + if ( defined( $decode_json->{listed} ) ); + readingsSingleUpdate( $hash, '.packageList', $json, 0 ); + } + elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^install/ ) { + delete $hash->{".fhem"}{installer}{installedpackages}; + $hash->{".fhem"}{installer}{installedpackages} = $decode_json; + readingsSingleUpdate( $hash, '.installedList', $json, 0 ); + } + elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^uninstall/ ) { + delete $hash->{".fhem"}{installer}{uninstalledpackages}; + $hash->{".fhem"}{installer}{uninstalledpackages} = $decode_json; + readingsSingleUpdate( $hash, '.uninstalledList', $json, 0 ); + } + elsif ( $hash->{".fhem"}{installer}{cmd} =~ /^update/ ) { + delete $hash->{".fhem"}{installer}{updatedpackages}; + $hash->{".fhem"}{installer}{updatedpackages} = $decode_json; + readingsSingleUpdate( $hash, '.updatedList', $json, 0 ); + } + + if ( defined( $decode_json->{warning} ) + or defined( $decode_json->{error} ) ) + { + $hash->{".fhem"}{installer}{'warnings'} = $decode_json->{warning} + if ( defined( $decode_json->{warning} ) ); + $hash->{".fhem"}{installer}{errors} = $decode_json->{error} + if ( defined( $decode_json->{error} ) ); + } + else { + delete $hash->{".fhem"}{installer}{'warnings'}; + delete $hash->{".fhem"}{installer}{errors}; + } + + WriteReadings( $hash, $decode_json ); +} + +sub WriteReadings($$) { + + my ( $hash, $decode_json ) = @_; + + my $name = $hash->{NAME}; + + Log3 $hash, 4, "Installer ($name) - Write Readings"; + Log3 $hash, 5, "Installer ($name) - " . Dumper $decode_json; + + readingsBeginUpdate($hash); + + if ( $hash->{".fhem"}{installer}{cmd} eq 'outdated' ) { + readingsBulkUpdate( + $hash, + 'outdated', + ( + defined( $decode_json->{listed} ) + ? 'check completed' + : 'check failed' + ) + ); + $hash->{helper}{lastSync} = ToDay(); + } + + readingsBulkUpdateIfChanged( $hash, 'updatesAvailable', + scalar keys %{ $decode_json->{outdated} } ) + if ( $hash->{".fhem"}{installer}{cmd} eq 'outdated' ); + readingsBulkUpdateIfChanged( $hash, 'updateListAsJSON', + eval { encode_json( $hash->{".fhem"}{installer}{outdatedpackages} ) } ) + if ( AttrVal( $name, 'updateListReading', 'none' ) ne 'none' ); + + my $result = 'successful'; + $result = 'error' if ( defined( $hash->{".fhem"}{installer}{errors} ) ); + $result = 'warning' + if ( defined( $hash->{".fhem"}{installer}{'warnings'} ) ); + + readingsBulkUpdate( $hash, 'installed', $result ) + if ( $hash->{".fhem"}{installer}{cmd} =~ /^install/ ); + readingsBulkUpdate( $hash, 'uninstalled', $result ) + if ( $hash->{".fhem"}{installer}{cmd} =~ /^uninstall/ ); + readingsBulkUpdate( $hash, 'updated', $result ) + if ( $hash->{".fhem"}{installer}{cmd} =~ /^update/ ); + + readingsBulkUpdateIfChanged( $hash, "nodejsVersion", + $decode_json->{versions}{node} ) + if ( defined( $decode_json->{versions} ) + && defined( $decode_json->{versions}{node} ) ); + + if ( defined( $decode_json->{error} ) ) { + readingsBulkUpdate( $hash, 'state', + 'error \'' . $hash->{".fhem"}{installer}{cmd} . '\'' ); + } + elsif ( defined( $decode_json->{warning} ) ) { + readingsBulkUpdate( $hash, 'state', + 'warning \'' . $hash->{".fhem"}{installer}{cmd} . '\'' ); + } + else { + + readingsBulkUpdate( + $hash, 'state', + ( + ( + scalar keys %{ $decode_json->{outdated} } > 0 + or scalar + keys %{ $hash->{".fhem"}{installer}{outdatedpackages} } > + 0 + ) + ? 'npm updates available' + : 'npm is up to date' + ) + ); + } + + Event( $hash, "FINISH" ); + readingsEndUpdate( $hash, 1 ); + + ProcessUpdateTimer($hash) + if ( $hash->{".fhem"}{installer}{cmd} eq 'getFhemVersion' + && !defined( $decode_json->{error} ) ); +} + +sub CreateMetadataList ($$$) { + my ( $hash, $getCmd, $modName ) = @_; + $modName = 'Global' if ( uc($modName) eq 'FHEM' ); + + return 'Unknown module ' . $modName + unless ( defined( $modules{$modName} ) ); + + FHEM::Meta::Load($modName); + + return 'No metadata found about module ' . $modName + unless ( defined( $modules{$modName}{META} ) + && scalar keys %{ $modules{$modName}{META} } > 0 ); + + my $modMeta = $modules{$modName}{META}; + my @ret; + my $html = defined( $hash->{CL} ) && $hash->{CL}{TYPE} eq "FHEMWEB" ? 1 : 0; + + my $header = ''; + my $footer = ''; + if ($html) { + $header = ''; + $footer = ''; + } + + my $tableOpen = ''; + my $rowOpen = ''; + my $rowOpenEven = ''; + my $rowOpenOdd = ''; + my $colOpen = ''; + my $colOpenMinWidth = ''; + my $txtOpen = ''; + my $txtClose = ''; + my $colClose = "\t\t\t"; + my $rowClose = ''; + my $tableClose = ''; + my $colorRed = ''; + my $colorGreen = ''; + my $colorClose = ''; + + if ($html) { + $tableOpen = ''; + $rowOpen = ''; + $rowOpenEven = ''; + $rowOpenOdd = ''; + $colOpen = ''; + $rowClose = ''; + $tableClose = '
'; + $colOpenMinWidth = ''; + $txtOpen = ""; + $txtClose = ""; + $colClose = '
'; + $colorRed = ''; + $colorGreen = ''; + $colorClose = ''; + } + + my @mAttrs = qw( + name + abstract + keywords + version + release_date + release_status + author + copyright + privacy + license + homepage + wiki + command_reference + community_support + commercial_support + bugtracker + version_control + description + ); + + my $space = $html ? ' ' : ' '; + my $lb = $html ? '
' : "\n"; + my $lang = lc( + AttrVal( + $hash->{NAME}, 'language', + AttrVal( 'global', 'language', 'EN' ) + ) + ); + + push @ret, $tableOpen; + + my $linecount = 1; + foreach my $mAttr (@mAttrs) { + next + if ( $mAttr eq 'copyright' && !defined( $modMeta->{x_copyright} ) ); + next + if ( + $mAttr eq 'privacy' + && ( !defined( $modMeta->{resources} ) + || !defined( $modMeta->{resources}{x_privacy} ) ) + ); + + my $l = $linecount % 2 == 0 ? $rowOpenEven : $rowOpenOdd; + my $mAttrName = $mAttr; + $mAttrName =~ s/_/$space/g; + $mAttrName =~ s/([\w'&]+)/\u\L$1/g; + + $l .= $colOpenMinWidth . $txtOpen . $mAttrName . $txtClose . $colClose; + + if ( !defined( $modMeta->{$mAttr} ) ) { + $l .= $colOpen; + + if ( $mAttr eq 'release_date' ) { + if ( defined( $modMeta->{x_vcs} ) ) { + $l .= $modMeta->{x_vcs}[7]; + } + else { + $l .= $modMeta->{x_file}[6][8][2] . ' (last modify date)'; + } + } + + elsif ( $mAttr eq 'copyright' ) { + my $copyName; + my $copyEmail; + my $copyWeb; + my $copyNameContact; + + if ( $modMeta->{x_copyright} =~ + m/^([^<>\n\r]+)(?:\s+(?:<(.*)>))?$/ ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $copyName = '© ' . $modMeta->{x_vcs}[8] . ' ' . $1; + } + else { + $copyName = '© ' . $1; + } + $copyEmail = $2; + } + if ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_copyright} ) + && defined( $modMeta->{resources}{x_copyright}{web} ) ) + { + $copyWeb = $modMeta->{resources}{x_copyright}{web}; + } + + if ( $html && $copyEmail ) { + $copyNameContact = + '' + . $copyName . ''; + } + elsif ( $html && $copyEmail ) { + $copyNameContact = + '' + . $copyName . ''; + } + + $l .= $copyNameContact ? $copyNameContact : $copyName; + } + + elsif ( $mAttr eq 'privacy' ) { + my $title = + defined( $modMeta->{resources}{x_privacy}{title} ) + ? $modMeta->{resources}{x_privacy}{title} + : $modMeta->{resources}{x_privacy}{web}; + + $l .= + '' + . $title . ''; + } + + elsif ($mAttr eq 'homepage' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{homepage} ) ) + { + my $title = + defined( $modMeta->{resources}{x_homepage_title} ) + ? $modMeta->{resources}{x_homepage_title} + : ( + $modMeta->{resources}{homepage} =~ m/^.+:\/\/([^\/]+).*/ + ? $1 + : $modMeta->{resources}{homepage} + ); + + $l .= + '' + . $title . ''; + } + + elsif ($mAttr eq 'command_reference' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_commandref} ) + && defined( $modMeta->{resources}{x_commandref}{web} ) ) + { + my $title = + defined( $modMeta->{resources}{x_commandref}{title} ) + ? $modMeta->{resources}{x_commandref}{title} + : ( + $modMeta->{resources}{x_commandref}{web} =~ + m/^(?:https?:\/\/)?fhem\.de/i + ? 'FHEM Public Command Reference' + : '' + ); + + my $url = + $modMeta->{resources}{x_commandref}{web}; + + if ( defined( $modMeta->{resources}{x_commandref}{modpath} ) ) { + $url .= + $modMeta->{resources}{x_commandref}{modpath}; + $url .= $modName eq 'Global' ? 'global' : $modName; + } + + $l .= + '' . $title . ''; + } + + elsif ($mAttr eq 'wiki' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_wiki} ) + && defined( $modMeta->{resources}{x_wiki}{web} ) ) + { + my $title = + defined( $modMeta->{resources}{x_wiki}{title} ) + ? $modMeta->{resources}{x_wiki}{title} + : ( + $modMeta->{resources}{x_wiki}{web} =~ + m/^(?:https?:\/\/)?wiki\.fhem\.de/i ? 'FHEM Wiki' + : '' + ); + + $title = 'FHEM Wiki: ' . $title + if ( $title ne '' + && $title !~ m/^FHEM Wiki/i + && $modMeta->{resources}{x_wiki}{web} =~ + m/^(?:https?:\/\/)?wiki\.fhem\.de/i ); + + my $url = + $modMeta->{resources}{x_wiki}{web}; + $url .= '/' unless ( $url =~ m/\/$/ ); + + if ( defined( $modMeta->{resources}{x_wiki}{modpath} ) ) { + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= + $modMeta->{resources}{x_wiki}{modpath}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modName eq 'Global' ? 'global' : $modName; + } + + $l .= + '' . $title . ''; + } + + elsif ($mAttr eq 'bugtracker' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{bugtracker} ) + && defined( $modMeta->{resources}{bugtracker}{web} ) ) + { + my $title = + defined( $modMeta->{resources}{bugtracker}{x_web_title} ) + ? $modMeta->{resources}{bugtracker}{x_web_title} + : ( + $modMeta->{resources}{bugtracker}{web} =~ + m/^(?:https?:\/\/)?forum\.fhem\.de/i ? 'FHEM Forum' + : '' + ); + + $title = 'FHEM Forum: ' . $title + if ( $title ne '' + && $title !~ m/^FHEM Forum/i + && $modMeta->{resources}{bugtracker}{web} =~ + m/^(?:https?:\/\/)?forum\.fhem\.de/i ); + + $l .= + '' + . $title . ''; + } + + elsif ($mAttr eq 'version_control' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{repository} ) + && defined( $modMeta->{resources}{repository}{type} ) + && defined( $modMeta->{resources}{repository}{url} ) ) + { + # Web link + if ( defined( $modMeta->{resources}{repository}{web} ) ) { + my $url = + $modMeta->{resources}{repository}{web}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{resources}{repository}{x_branch_master} + if ( + defined( + $modMeta->{resources}{repository}{x_branch_master} + ) + ); + + if ( + defined( + $modMeta->{resources}{repository}{x_filepath} + ) + ) + { + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= + $modMeta->{resources}{repository}{x_filepath}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{x_file}[2]; + + } + + $l .= + 'Web: ' + . ( + defined( + $modMeta->{resources}{repository}{x_web_title} + ) + ? $modMeta->{resources}{repository}{x_web_title} + : $url + ) . ''; + + if ( + defined( + $modMeta->{resources}{repository}{x_branch_master} + ) + && defined( + $modMeta->{resources}{repository}{x_branch_dev} + ) + && $modMeta->{resources}{repository}{x_branch_master} + ne $modMeta->{resources}{repository}{x_branch_dev} + ) + { + my $url = + $modMeta->{resources}{repository}{web}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{resources}{repository}{x_branch_dev}; + + $l .= + ' Development branch: ' + . $modMeta->{resources}{repository}{x_branch_dev} + . ''; + } + + $l .= $lb; + } + + # VCS link + my $url = + $modMeta->{resources}{repository}{url}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{resources}{repository}{x_branch_master} + if ( + defined( + $modMeta->{resources}{repository}{x_branch_master} + ) + ); + + if ( defined( $modMeta->{resources}{repository}{x_filepath} ) ) + { + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= + $modMeta->{resources}{repository}{x_filepath}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{x_file}[2]; + + } + + $l .= + $modMeta->{resources}{repository}{type} + . ': ' + . $url . ''; + + if ( + defined( + $modMeta->{resources}{repository}{x_branch_master} + ) + && defined( + $modMeta->{resources}{repository}{x_branch_dev} ) + && $modMeta->{resources}{repository}{x_branch_master} ne + $modMeta->{resources}{repository}{x_branch_dev} + ) + { + my $url = + $modMeta->{resources}{repository}{url}; + $url .= '/' unless ( $url =~ m/\/$/ ); + $url .= $modMeta->{resources}{repository}{x_branch_dev}; + + $l .= + ' Development branch: ' + . $modMeta->{resources}{repository}{x_branch_dev} + . ''; + } + } + else { + $l .= '-'; + } + + $l .= $colClose; + } + elsif ( !ref( $modMeta->{$mAttr} ) ) { + $l .= $colOpen; + + my $mAttrVal = + defined( $modMeta->{x_lang} ) + && defined( $modMeta->{x_lang}{$lang} ) + && defined( $modMeta->{x_lang}{$lang}{$mAttr} ) + ? $modMeta->{x_lang}{$lang}{$mAttr} + : $modMeta->{$mAttr}; + $mAttrVal =~ s/\\n/$lb/g; + + if ( $mAttr eq 'license' + && defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{license} ) + && ref( $modMeta->{resources}{license} ) eq 'ARRAY' + && @{ $modMeta->{resources}{license} } > 0 + && $modMeta->{resources}{license}[0] ne '' ) + { + $mAttrVal = + '' + . $mAttrVal . ''; + } + elsif ( $mAttr eq 'version' && $modName ne 'Global' ) { + if ( $modMeta->{x_file}[7] ne 'generated/vcs' + && defined( $modMeta->{x_vcs} ) + && $modMeta->{x_vcs}[5] ne '' ) + { + $mAttrVal .= '-s' . $modMeta->{x_vcs}[5]; + } + } + + $mAttrVal .= ' (' . $modMeta->{x_file}[2] . ')' + if ( $mAttr eq 'name' && $modName ne 'Global' ); + + $l .= $mAttrVal . $colClose; + } + elsif (ref( $modMeta->{$mAttr} ) eq 'ARRAY' + && @{ $modMeta->{$mAttr} } > 0 + && $modMeta->{$mAttr}[0] ne '' ) + { + $l .= $colOpen; + + if ( $mAttr eq 'author' ) { + my $authorCount = scalar @{ $modMeta->{$mAttr} }; + my $counter = 0; + + foreach ( @{ $modMeta->{$mAttr} } ) { + next if ( $_ eq '' ); + + my $authorName; + my $authorEmail; + my $authorNameEmail; + + if ( $_ =~ m/^([^<>\n\r]+)(?:\s+(?:<(.*)>))?$/ ) { + $authorName = $1; + $authorEmail = $2; + } + + $authorNameEmail = + '' + . $authorName . '' + if ( $html && $authorEmail ); + + if ( defined( $modMeta->{x_fhem_maintainer} ) + && ref( $modMeta->{x_fhem_maintainer} ) eq 'ARRAY' + && @{ $modMeta->{x_fhem_maintainer} } > 0 + && $modMeta->{x_fhem_maintainer}[$counter] ne '' ) + { + $authorNameEmail = ( + $authorNameEmail + ? $authorNameEmail + : $authorName + ) + . ', alias ' + . $modMeta->{x_fhem_maintainer}[$counter] + if ( $modMeta->{x_fhem_maintainer}[$counter] ne + $authorName ); + } + + $l .= $lb if ($counter); + $l .= $lb . 'Co-' . $mAttrName . ':' . $lb + if ( $counter == 1 ); + $l .= $authorNameEmail ? $authorNameEmail : $authorName; + + $counter++; + } + } + else { + $l .= join ', ', @{ $modMeta->{$mAttr} }; + } + + $l .= $colClose; + } + else { + $l .= $colOpen . '?' . $colClose; + } + + $l .= $rowClose; + + push @ret, $l; + $linecount++; + } + + push @ret, $tableClose; + + push @ret, '

System Prerequisites

'; + + my $moduleUsage = + defined( $modules{$modName}{LOADED} ) + ? $colorGreen . 'IN USE' . $colorClose + : $txtOpen . 'not' . $txtClose . ' in use'; + + push @ret, 'This FHEM module is currently ' . $moduleUsage . '.' + unless ( $modName eq 'Global' ); + + push @ret, '

Perl Modules

'; + if ( defined( $modMeta->{prereqs} ) + && defined( $modMeta->{prereqs}{runtime} ) ) + { + + push @ret, + $txtOpen . 'HINT:' + . $txtClose . "\n" + . 'This module does not provide prerequisites from its metadata.' + . "\n" + . 'The following result is based on automatic source code analysis' + . "\n" + . 'and can be incorrect.' + . $lb + if ( !defined( $modMeta->{x_prereqs_src} ) + && $modMeta->{x_prereqs_src} ne 'META.json' ); + + my @mAttrs = qw( + requires + recommends + suggests + ); + + push @ret, $tableOpen; + + push @ret, $colOpenMinWidth . $txtOpen . 'Name' . $txtClose . $colClose; + + push @ret, + $colOpenMinWidth . $txtOpen . 'Importance' . $txtClose . $colClose; + + push @ret, + $colOpenMinWidth . $txtOpen . 'Status' . $txtClose . $colClose; + + my $linecount = 1; + foreach my $mAttr (@mAttrs) { + next + unless ( defined( $modMeta->{prereqs}{runtime}{$mAttr} ) + && keys %{ $modMeta->{prereqs}{runtime}{$mAttr} } > 0 ); + + foreach + my $prereq ( sort keys %{ $modMeta->{prereqs}{runtime}{$mAttr} } ) + { + my $l = $linecount % 2 == 0 ? $rowOpenEven : $rowOpenOdd; + + my $importance = $mAttr; + $importance = 'required' if ( $mAttr eq 'requires' ); + $importance = 'recommended' if ( $mAttr eq 'recommends' ); + $importance = 'suggested' if ( $mAttr eq 'suggests' ); + + my $version = $modMeta->{prereqs}{runtime}{$mAttr}{$prereq}; + $version = '' if ( !defined($version) || $version eq '0' ); + $version = version->parse($version)->normal + if ( $version ne '' ); + + my $check = __IsInstalledPerl($prereq); + my $installed = ''; + if ($check) { + if ( $check =~ m/^v/ ) { + my $nverReq = + $version ne '' + ? version->parse($version)->numify + : 0; + my $nverInst = version->parse($check)->numify; + + if ( $nverReq > 0 && $nverInst < $nverReq ) { + $installed .= 'OUTDATED (' . $check . ')'; + } + else { + $installed = 'installed'; + } + } + else { + $installed = 'installed'; + } + } + else { + $installed = $colorRed . 'MISSING' . $colorClose + if ( $importance eq 'required' ); + } + + $prereq = + '' + . $prereq . '' + if ($html); + + $l .= + $colOpenMinWidth + . $prereq + . ( $version ne '' ? " ($version)" : '' ) + . $colClose; + $l .= $colOpenMinWidth . $importance . $colClose; + $l .= $colOpenMinWidth . $installed . $colClose; + + $l .= $rowClose; + + push @ret, $l; + $linecount++; + } + } + + push @ret, $tableClose; + } + elsif ( defined( $modMeta->{x_prereqs_src} ) ) { + push @ret, 'No known prerequisites.' . $lb . $lb; + } + else { + push @ret, + 'Metadata does not contain any prerequisites.' . "\n" + . 'For automatic source code analysis, please install Perl::PrereqScanner::NotQuiteLite .' + . $lb + . $lb; + } + + push @ret, 'Based on data generated by ' . $modMeta->{generated_by}; + + return $header . join( "\n", @ret ) . $footer; + +} + +sub __IsInstalledPerl($) { + return 0 unless ( __PACKAGE__ eq caller(0) ); + return 0 unless (@_); + my ($pkg) = @_; + return version->parse($])->normal if ( $pkg eq 'perl' ); + return version->parse( $modules{'Global'}{META}{version} )->normal + if ( $pkg eq 'FHEM' ); + + eval "require $pkg;"; + + return 0 + if ($@); + + my $v = eval "$pkg->VERSION()"; + + if ($v) { + return version->parse($v)->normal; + } + else { + return 1; + } +} + +#### my little helper +sub ToDay() { + + my ( $sec, $min, $hour, $mday, $month, $year, $wday, $yday, $isdst ) = + localtime( gettimeofday() ); + + $month++; + $year += 1900; + + my $today = sprintf( '%04d-%02d-%02d', $year, $month, $mday ); + + return $today; +} + +1; + +=pod +=encoding utf8 +=item helper +=item summary Module to help with FHEM installations +=item summary_DE Modul zur Unterstuetzung bei FHEM Installationen + +=begin html + + +

+ Installer +

+
    + Installer - Module to update FHEM, install 3rd-party FHEM modules and manage system prerequisites
    +
    +
    + Define
    +
      + define <name> Installer
      +
      + Example:
      +
        + define fhemInstaller Installer
        +

      +

    +
    + Get +
      +
    • showModuleInfo - list information about a specific FHEM module +
    • +

    +
    + Attributes +
      +
    • disable - disables the device +
    • +
    • disabledForIntervals - disable device for interval time (13:00-18:30 or 13:00-18:30 22:00-23:00) +
    • +
    +
+ +=end html + +=begin html_DE + +

+ +

+

+ Installer +

+
    + Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden: +
+ + +=end html_DE + +=for :application/json;q=META.json 98_Installer.pm +{ + "abstract": "Module to update FHEM, install 3rd-party FHEM modules and manage system prerequisites", + "x_lang": { + "de": { + "abstract": "Modul zum Update von FHEM, zur Installation von Drittanbieter FHEM Modulen und der Verwaltung von Systemvoraussetzungen" + } + }, + "keywords": [ + "fhem-core", + "fhem-mod", + "fhem-mod-helper", + "fhem-3rdparty" + ], + "version": "v0.0.1", + "release_status": "testing", + "author": [ + "Julian Pawlowski " + ], + "x_fhem_maintainer": [ + "loredo" + ], + "x_fhem_maintainer_github": [ + "jpawlowski" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918623, + "perl": 5.014, + "GPUtils": 0, + "JSON": 0, + "FHEM::Meta": 0, + "Data::Dumper": 0, + "IO::Socket::SSL": 0, + "HttpUtils": 0, + "File::stat": 0, + "Encode": 0 + }, + "recommends": { + "Perl::PrereqScanner::NotQuiteLite": 0, + "Time::Local": 0 + }, + "suggests": { + } + } + }, + "resources": { + "bugtracker": { + "web": "https://forum.fhem.de/index.php/board,44.0.html", + "x_web_title": "FHEM Forum: Unterstützende Dienste" + } + } +} +=end :application/json;q=META.json + +=cut diff --git a/fhem/FHEM/Meta.pm b/fhem/FHEM/Meta.pm index 3f21a795c..603db460a 100644 --- a/fhem/FHEM/Meta.pm +++ b/fhem/FHEM/Meta.pm @@ -37,37 +37,25 @@ return "$@" if ($@); return $ret if ($ret); use version 0.77; our $VERSION = $META{version}; -our $coreUpdate; -our %corePackageUpdates; -our %coreFileUpdates; +# sub import(@) { +# my $pkg = caller(0); +# +# if ( $pkg ne "main" ) { +# } +# } -our %moduleUpdates; -our %packageUpdates; -our %fileUpdates; - -sub import(@) { - my $pkg = caller(0); - - # Initially load update information - # to be ready for meta analysis - __GetUpdatedata() unless ( defined($coreUpdate) ); - - if ( $pkg ne "main" ) { - } -} - -# Loads Metadata for a module -sub Load($$;$) { +# Loads Metadata for single module, based on filename +sub InitMod($$;$) { my ( $filePath, $modHash, $runInLoop ) = @_; my $ret = __PutMetadata( $filePath, $modHash, 1, $runInLoop ); if ($@) { - Log 1, __PACKAGE__ . "::Load: ERROR: \$\@:\n" . $@; + Log 1, __PACKAGE__ . "::InitMod: ERROR: \$\@:\n" . $@; return "$@"; } elsif ($ret) { - Log 1, __PACKAGE__ . "::Load: ERROR: \$ret:\n" . $ret; + Log 1, __PACKAGE__ . "::InitMod: ERROR: \$ret:\n" . $ret; return $ret; } @@ -92,33 +80,70 @@ sub Load($$;$) { return undef; } -#TODO allow to have array of module names as optional parameter, use keys %modules when not given -# Then make this function to be called by X_Initialize(). Problem: We don't know the module name yet, just filename. -# So maybe one can give wither filepath or modulename as parameter? -# Load Metadata for non-loaded modules -sub LoadAll(;$$) { - my ( $unused, $reload ) = @_; +# Load Metadata for a list of modules +sub Load(;$$) { + my ( $modList, $reload ) = @_; my $t = TimeNow(); my $v = __PACKAGE__->VERSION(); my @rets; + my $unused = 0; + my @lmodules; + + # if modList is undefined or is equal to '1' + if ( !$modList || ( !ref($modList) && $modList eq '1' ) ) { + $unused = 1 if ( $modList && $modList eq '1' ); + + foreach ( keys %modules ) { + + # Only process loaded modules + # unless unused modules were + # explicitly requested + push @lmodules, + $_ + if ( + $unused + || ( defined( $modules{$_}{LOADED} ) + && $modules{$_}{LOADED} eq '1' ) + ); + } + } + + # if a single module name was given + elsif ( !ref($modList) ) { + push @lmodules, $modList; + } + + # if a list of module names was given + elsif ( ref($modList) eq 'ARRAY' ) { + foreach ( @{$modList} ) { + push @lmodules, $_; + } + } + + # if a hash was given, assume every + # key is a module name + elsif ( ref($modList) eq 'HASH' ) { + foreach ( keys %{$modList} ) { + push @lmodules, $_; + } + } + + # Wrong method use + else { + $@ = __PACKAGE__ . "::Load: ERROR: Unknown parameter value"; + Log 1, $@; + return "$@"; + } + __GetUpdatedata() if ($reload); - foreach my $modName ( keys %modules ) { - - # Only add META to loaded modules - # if not enforced for all - next - unless ( - $unused - || ( defined( $modules{$modName}{LOADED} ) - && $modules{$modName}{LOADED} eq '1' ) - ); + foreach my $modName (@lmodules) { # Abort when module file was not indexed by # fhem.pl before. # Only continue if META was not loaded - # or should explicitly reloaded. + # or should explicitly be reloaded. next if ( !defined( $modules{$modName}{ORDER} ) @@ -141,15 +166,17 @@ sub LoadAll(;$$) { . $modName . '.pm'; } - my $ret = Load( $filePath, $modules{$modName}, 1 ); - push @rets, $@ if ( $@ && $@ ne "" ); - push @rets, $ret if ( $ret && $ret ne "" ); + my $ret = InitMod( $filePath, $modules{$modName}, 1 ); + push @rets, $@ if ( $@ && $@ ne '' ); + push @rets, $ret if ( $ret && $ret ne '' ); $modules{$modName}{META}{generated_by} = $META{name} . " $v, $t" if ( defined( $modules{$modName}{META} ) ); - } - SetInternals( $defs{'global'} ); + foreach my $devName ( devspec2array( 'TYPE=' . $modName ) ) { + SetInternals( $defs{$devName} ); + } + } if (@rets) { $@ = join( "\n", @rets ); @@ -176,13 +203,13 @@ sub SetInternals($) { return 0 unless ( defined( $modHash->{LOADED} ) && $modHash->{LOADED} eq '1' ); - $devHash->{'.FhemMetaInternalss'} = 1; + $devHash->{'.FhemMetaInternals'} = 1; __CopyMetaToInternals( $devHash, $modMeta ); return 1; } -# Get meta data +# Get metadata sub Get($$) { my ( $devHash, $field ) = @_; $devHash = $defs{$devHash} unless ( ref($devHash) ); @@ -208,10 +235,10 @@ sub Get($$) { sub __CopyMetaToInternals { return 0 unless ( __PACKAGE__ eq caller(0) ); my ( $devHash, $modMeta ) = @_; - return unless ( defined( $devHash->{'.FhemMetaInternalss'} ) ); + return unless ( defined( $devHash->{'.FhemMetaInternals'} ) ); return unless ( defined($modMeta) && ref($modMeta) eq "HASH" ); - $devHash->{FUPDATE} = $modMeta->{x_version} + $devHash->{FVERSION} = $modMeta->{x_version} if ( defined( $modMeta->{x_version} ) ); } @@ -238,7 +265,7 @@ sub __PutMetadata { return undef; } -# Extract meta data from FHEM module file +# Extract metadata from FHEM module file sub __GetMetadata { return 0 unless ( __PACKAGE__ eq caller(0) ); my ( $filePath, $modMeta, $runInLoop, $metaSection ) = @_; @@ -287,9 +314,9 @@ sub __GetMetadata { } my $searchComments = 1; # not in use, see below - my $currentJson = ""; + my $currentJson = ''; while ( my $l = <$fh> ) { - next if ( $l eq "" || $l =~ m/^\s+$/ ); + next if ( $l eq '' || $l =~ m/^\s+$/ ); # # Track comments section at the beginning of the document # if ( $searchComments && $l !~ m/^#|\s*$/ ) { @@ -338,7 +365,7 @@ m/(\$Id\: ((?:([0-9]+)_)?([\w]+)\.([\w]+))\s([0-9]+)\s((([0-9]+)-([0-9]+)-([0-9] # $authorName = $authorMail # if ( $authorName && $authorName =~ m/written| from| by/i ); # -# $authorName = "" unless ($authorName); +# $authorName = '' unless ($authorName); # } ###### @@ -423,7 +450,7 @@ m/^=for\s+:application\/json;q=META\.json\s+([^\s\.]+\.[^\s\.]+)\s*$/i { $skip = 0; $currentJson = $1; - $json{$currentJson} = ""; + $json{$currentJson} = ''; } elsif ( !$skip && $l =~ m/^=end\s+:application\/json\;q=META\.json/i ) @@ -441,7 +468,7 @@ m/^=for\s+:application\/json;q=META\.json\s+([^\s\.]+\.[^\s\.]+)\s*$/i seek $fh, 0, 0; while ( my $l = <$fh> ) { - next if ( $l eq "" || $l =~ m/^\s+$/ ); + next if ( $l eq '' || $l =~ m/^\s+$/ ); # Only seek the document until code starts if ( $l !~ m/^#/ && $l !~ m/^=[A-Za-z]+/i ) { @@ -649,7 +676,7 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? } ######## - # Meta data refactoring starts here + # Metadata refactoring starts here # #TODO @@ -702,7 +729,7 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? push @{ $modMeta->{x_file} }, $version; # Do not use repeating 0 in version - $modMeta->{version} =~ s/\.0{2,}/\.0/g + $modMeta->{version} = version->parse( $modMeta->{version} )->stringify if ( defined( $modMeta->{version} ) ); $@ .= @@ -740,14 +767,8 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? ); $modMeta->{abstract} = $item_summary if ( $item_summary && !defined( $modMeta->{abstract} ) ); - $modMeta->{x_lang}{DE}{abstract} = $item_summary_DE - if ( $item_summary_DE && !defined( $modMeta->{x_lang}{DE}{abstract} ) ); - - $modMeta->{description} = "./docs/commandref.html#" . $modMeta->{x_file}[4] - unless ( defined( $modMeta->{description} ) ); - $modMeta->{x_lang}{DE}{description} = - "./docs/commandref_DE.html#" . $modMeta->{x_file}[4] - unless ( defined( $modMeta->{x_lang}{DE}{description} ) ); + $modMeta->{x_lang}{de}{abstract} = $item_summary_DE + if ( $item_summary_DE && !defined( $modMeta->{x_lang}{de}{abstract} ) ); # Only when this package is reading its own metadata. # Other modules shall get this added elsewhere for performance reasons @@ -762,6 +783,12 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? $META{name} . ' ' . __PACKAGE__->VERSION() . ', ' . TimeNow(); } + # mandatory + unless ( $modMeta->{description} ) { + $modMeta->{description} = 'n/a'; + } + + # mandatory unless ( $modMeta->{release_status} ) { if ( defined( $modMeta->{x_vcs} ) ) { $modMeta->{release_status} = 'stable'; @@ -771,6 +798,7 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? } } + # mandatory unless ( $modMeta->{license} ) { if ( defined( $modMeta->{x_vcs} ) ) { $modMeta->{license} = 'GPL_2'; @@ -780,6 +808,7 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? } } + # mandatory unless ( $modMeta->{author} ) { if ( defined( $modMeta->{x_vcs} ) ) { $modMeta->{author} = [ $modMeta->{x_vcs}[15] . ' <>' ]; @@ -797,6 +826,66 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? } } + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{license} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{license} = + ['https://fhem.de/#License']; + } + } + + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{bugtracker} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{bugtracker}{web} = 'https://forum.fhem.de/'; + } + } + + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_wiki} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{x_wiki}{web} = 'https://wiki.fhem.de/'; + $modMeta->{resources}{x_wiki}{modpath} = 'wiki/'; + } + } + + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_commandref} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{x_commandref}{web} = + 'https://fhem.de/commandref.html'; + $modMeta->{resources}{x_commandref}{modpath} = '#'; + } + } + + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{x_support_community} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{x_support_community}{web} = + 'https://forum.fhem.de/'; + } + } + + unless ( defined( $modMeta->{resources} ) + && defined( $modMeta->{resources}{repository} ) ) + { + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{resources}{repository}{type} = 'svn'; + $modMeta->{resources}{repository}{web} = + 'https://svn.fhem.de/trac/browser/'; + $modMeta->{resources}{repository}{url} = + 'https://svn.fhem.de/fhem/'; + $modMeta->{resources}{repository}{x_branch_master} = 'trunk'; + $modMeta->{resources}{repository}{x_branch_dev} = 'trunk'; + $modMeta->{resources}{repository}{x_filepath} = 'fhem/FHEM/'; + } + } + # Static meta information $modMeta->{dynamic_config} = 1; $modMeta->{'meta-spec'} = { @@ -811,80 +900,13 @@ m/(^#\s+(?:\d{1,2}\.\d{1,2}\.(?:\d{2}|\d{4})\s+)?[^v\d]*(v?(?:\d{1,3}\.\d{1,3}(? return undef; } -# Set x_version based on existing meta data -sub __SetXVersion { - return 0 unless ( __PACKAGE__ eq caller(0) ); - my ($modMeta) = @_; - my $modName = $modMeta->{x_file}[4]; - - delete $modMeta->{x_version} if ( defined( $modMeta->{x_version} ) ); - - # Special handling for fhem.pl - if ( $modMeta->{x_file}[2] eq 'fhem.pl' ) { - $modMeta->{x_version} = 'fhem.pl:' . $modMeta->{version}; - } - - # Generate extended version info based - # on base revision - elsif ( defined( $modMeta->{x_file}[7] eq 'generated/vcs' ) ) { - - $modMeta->{x_version} = - $modMeta->{x_file}[2] . ':' - . ( - $modMeta->{version} =~ m/0+\.0+(?:\.0+)?$/ - ? '?' - : $modMeta->{version} - ); - } - - # Generate generic version to fill the gap - elsif ( defined( $modMeta->{x_file}[7] eq 'generated/blank' ) ) { - $modMeta->{x_version} = $modMeta->{x_file}[2] . ':?'; - } - - # Generate extended version info with added base revision - elsif ( defined( $modMeta->{x_vcs} ) - && $modMeta->{x_vcs}[5] ne '' ) - { - $modMeta->{x_version} = - $modMeta->{x_file}[2] . ':' - . ( - $modMeta->{version} =~ m/^v0+\.0+(?:\.0+)*?$/ - ? '?' - : $modMeta->{version} - ) - . '-s' # assume we only have Subversion for now - . $modMeta->{x_vcs}[5]; - } - - if ( defined( $modMeta->{x_version} ) ) { - - # Add modified date to extended version - if ( defined( $modMeta->{x_vcs} ) ) { - $modMeta->{x_version} .= '/' . $modMeta->{x_vcs}[7]; - - # #FIXME can't use modified time because FHEM Update currently - # # does not set it based on controls_fhem.txt :-( - # # We need the block size from controls_fhem.txt here but - # # doesn't make sense to load that file here... - # $modMeta->{x_version} .= '/' . $modMeta->{x_file}[6][9][2]; - # $modMeta->{x_version} .= '+modified' - # if ( defined( $modMeta->{x_vcs} ) - # && $modMeta->{x_vcs}[16] ne $modMeta->{x_file}[6][9][0] ); - } - else { - $modMeta->{x_version} .= '/' . $modMeta->{x_file}[6][9][2]; - } - } -} - sub __GetUpdatedata { return 0 unless ( __PACKAGE__ eq caller(0) ); my $fh; my @fileList; # if there are 3rd party source file repositories involved - if ( open( $fh, '<' . './FHEM/controls.txt' ) ) { + if ( open( $fh, '<' . $attr{global}{modpath} . '/FHEM/controls.txt' ) ) { while ( my $l = <$fh> ) { push @fileList, $1 if ( $l =~ m/([^\/\s]+)$/ ); } @@ -898,7 +920,7 @@ sub __GetUpdatedata { # loop through control files foreach my $file (@fileList) { - if ( open( $fh, '<' . './FHEM/' . $file ) ) { + if ( open( $fh, '<' . $attr{global}{modpath} . '/FHEM/' . $file ) ) { my $filePrefix; my $srcRepoName; my $fileExtension; @@ -1063,6 +1085,65 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) } } +# Set x_version based on existing metadata +sub __SetXVersion { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my ($modMeta) = @_; + my $modName = $modMeta->{x_file}[4]; + + delete $modMeta->{x_version} if ( defined( $modMeta->{x_version} ) ); + + # Special handling for fhem.pl + if ( $modMeta->{x_file}[2] eq 'fhem.pl' ) { + $modMeta->{x_version} = 'fhem.pl:' . $modMeta->{version}; + } + + # Generate extended version info based + # on base revision + elsif ( defined( $modMeta->{x_vcs} ) ) { + + $modMeta->{x_version} = + $modMeta->{x_file}[2] . ':' + . ( + $modMeta->{version} =~ m/^v0+\.0+(?:\.0+)*?$/ + ? '?' + : $modMeta->{version} + ) + . ( + $modMeta->{x_file}[7] ne 'generated/vcs' + && $modMeta->{x_vcs}[5] ne '' + ? '-s' # assume we only have Subversion for now + . $modMeta->{x_vcs}[5] + : '' + ); + } + + # Generate generic version to fill the gap + elsif ( defined( $modMeta->{x_file}[7] eq 'generated/blank' ) ) { + $modMeta->{x_version} = $modMeta->{x_file}[2] . ':?'; + } + + if ( defined( $modMeta->{x_version} ) ) { + + # Add modified date to extended version + if ( defined( $modMeta->{x_vcs} ) ) { + $modMeta->{x_version} .= '/' . $modMeta->{x_vcs}[7]; + + # #FIXME can't use modified time because FHEM Update currently + # # does not set it based on controls_fhem.txt :-( + # # We need the block size from controls_fhem.txt here but + # # doesn't make sense to load that file here... + # $modMeta->{x_version} .= '/' . $modMeta->{x_file}[6][9][2]; + # $modMeta->{x_version} .= '+modified' + # if ( defined( $modMeta->{x_vcs} ) + # && $modMeta->{x_vcs}[16] ne $modMeta->{x_file}[6][9][0] ); + } + else { + $modMeta->{x_version} .= '/' . $modMeta->{x_file}[6][9][2]; + } + } +} + 1; =pod @@ -1084,7 +1165,7 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) "metadata", "meta" ], - "version": "v0.1.2", + "version": "v0.1.3", "release_status": "testing", "author": [ "Julian Pawlowski " @@ -1206,20 +1287,9 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) } }, "resources": { - "license": [ - "https://fhem.de/#License" - ], - "homepage": "https://fhem.de/", "bugtracker": { "web": "https://forum.fhem.de/index.php/board,48.0.html", - "x_web_title": "FHEM Development" - }, - "repository": { - "type": "svn", - "url": "https://svn.fhem.de/fhem/", - "x_branch_master": "trunk", - "x_branch_dev": "trunk", - "web": "https://svn.fhem.de/" + "x_web_title": "FHEM Forum: FHEM Development" } } } @@ -1239,6 +1309,7 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) "fhem", "fhem-core" ], + "x_copyright": "FHEM e.V., Rudolf König ", "author": [ "Rudolf König " ], @@ -1271,7 +1342,6 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) }, "suggests": { "Compress::Zlib": 0, - "configDB": 0, "FHEM::WinService": 0, "IO::Socket::INET6": 0, "Socket6": 0 @@ -1396,20 +1466,21 @@ m/^((\S+) (((....)-(..)-(..))_((..):(..):(..))) (\d+) (?:\.\/)?((.+\/)?((?:(\d+) } }, "resources": { - "license": [ - "https://fhem.de/#License" - ], - "homepage": "https://fhem.de/", - "bugtracker": { - "web": "https://forum.fhem.de/", - "x_web_title": "FHEM Forum" + "x_copyright": { + "web": "https://verein.fhem.de/" }, + "x_privacy": { + "web": "https://fhem.de/Impressum.html#Datenschutz", + "title": "FHEM Data Privacy Statement" + }, + "homepage": "https://fhem.de/", "repository": { "type": "svn", + "web": "https://svn.fhem.de/trac/browser/", "url": "https://svn.fhem.de/fhem/", "x_branch_master": "trunk", "x_branch_dev": "trunk", - "web": "https://svn.fhem.de/" + "x_filepath": "fhem/" } } } diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 5bc78ed01..554f6ba53 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -476,6 +476,7 @@ FHEM/98_HTTPMOD.pm stefanstrobel Sonstiges FHEM/98_Hyperion.pm DeeSPe Beleuchtung FHEM/98_IF.pm damian-s Automatisierung FHEM/98_inotify.pm marvin78 Automatisierung +FHEM/98_Installer.pm loredo Unterstuetzende Dienste FHEM/98_JsonList2.pm rudolfkoenig Automatisierung FHEM/98_logProxy.pm justme1968 Frontends/SVG Plots logProxy FHEM/98_mark.pm betateilchen Sonstiges