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 = '
';
}
- 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 = '';
}
- 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 = '';
+ $colOpenMinWidth = ' | ';
+ $txtOpen = "";
+ $txtClose = "";
+ $colClose = ' | ';
+ $rowClose = '
';
+ $tableClose = '
';
+ $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