diff --git a/fhem/FHEM/HOMESTATEtk.pm b/fhem/FHEM/HOMESTATEtk.pm new file mode 100644 index 000000000..7a898337b --- /dev/null +++ b/fhem/FHEM/HOMESTATEtk.pm @@ -0,0 +1,1302 @@ +############################################################################### +# $Id$ +package main; +use strict; +use warnings; +use Data::Dumper; +use Time::Local; + +require RESIDENTStk; + +# module variables ############################################################ +my %stateSecurity = ( + en => [ 'unlocked', 'locked', 'protected', 'secured', 'guarded' ], + de => + [ 'unverriegelt', 'verriegelt', 'geschützt', 'gesichert', 'überwacht' ], + icons => [ + 'status_open', 'status_standby', + 'status_night', 'status_locked', + 'building_security' + ], +); + +my %stateOnoff = ( + en => [ 'off', 'on' ], + de => [ 'aus', 'an' ], +); + +my %readingsMap = ( + date_long => 'calTod', + date_short => 'calTodS', + daytime_long => 'daytime', + daytimeStage => 'daytimeStage', + daytimeStageLn => 'calTodDaytimeStageLn', + daytimeT => 'calTodDaytimeT', + day_desc => 'calTodDesc', + + # dstchange => 'calTodDSTchng', + + dst_long => 'calTodDST', + isholiday => 'calTodHoliday', + isly => 'calTodLeapyear', + iswe => 'calTodTodWeekend', + mday => 'calTodMonthday', + mdayrem => 'calTodMonthdayRem', + monISO => 'calTodMonthN', + mon_long => 'calTodMonth', + mon_short => 'calTodMonthS', + rday_long => 'calTodRel', + seasonAstroChng => 'calTodSAstroChng', + seasonAstro_long => 'calTodSAstro', + seasonMeteoChng => 'calTodSMeteoChng', + seasonMeteo_long => 'calTodSMeteo', + seasonPheno_long => 'calTodSPheno', + sunrise => 'calTodSunrise', + sunset => 'calTodSunset', + daytimeStages => 'daytimeStages', + wdaynISO => 'calTodWeekdayN', + wday_long => 'calTodWeekday', + wday_short => 'calTodWeekdayS', + weekISO => 'calTodWeek', + yday => 'calTodYearday', + ydayrem => 'calTodYeardayRem', + year => 'calTodYear', +); + +my %readingsMap_tom = ( + date_long => 'calTom', + date_short => 'calTomS', + daytimeStageLn => 'calTomDaytimeStageLn', + daytimeT => 'calTomDaytimeT', + day_desc => 'calTomDesc', + + # dstchange => 'calTomDSTchng', + + dst_long => 'calTomDST', + isholiday => 'calTomHoliday', + isly => 'calTomLeapyear', + iswe => 'calTomWeekend', + mday => 'calTomMonthday', + mdayrem => 'calTomMonthdayRem', + monISO => 'calTomMonthN', + mon_long => 'calTomMonth', + mon_short => 'calTomMonthS', + rday_long => 'calTomRel', + seasonAstroChng => 'calTomSAstroChng', + seasonAstro_long => 'calTomSAstro', + seasonMeteoChng => 'calTomSMeteoChng', + seasonMeteo_long => 'calTomSMeteo', + seasonPheno_long => 'calTodSPheno', + sunrise => 'calTomSunrise', + sunset => 'calTomSunset', + wdaynISO => 'calTomWeekdayN', + wday_long => 'calTomWeekday', + wday_short => 'calTomWeekdayS', + weekISO => 'calTomWeek', + yday => 'calTomYearday', + ydayrem => 'calTomYeardayRem', + year => 'calTomYear', +); + +# initialize ################################################################## +sub HOMESTATEtk_Initialize($) { + my ($hash) = @_; + + $hash->{InitDevFn} = "HOMESTATEtk_InitializeDev"; + $hash->{DefFn} = "HOMESTATEtk_Define"; + $hash->{UndefFn} = "HOMESTATEtk_Undefine"; + $hash->{SetFn} = "HOMESTATEtk_Set"; + $hash->{GetFn} = "HOMESTATEtk_Get"; + $hash->{AttrFn} = "HOMESTATEtk_Attr"; + $hash->{NotifyFn} = "HOMESTATEtk_Notify"; + $hash->{parseParams} = 1; + + $hash->{AttrList} = + "disable:1,0 disabledForIntervals do_not_notify:1,0 " + . $readingFnAttributes + . " Lang:EN,DE DebugDate "; + + my $holidayFilter = + $attr{global}{holiday2we} + ? ":FILTER=NAME!=" . $attr{global}{holiday2we} + : ""; + $hash->{AttrList} .= + " HolidayDevices:multiple," + . join( ",", devspec2array( "TYPE=holiday" . $holidayFilter ) ); + $hash->{AttrList} .= " ResidentsDevices:multiple," + . join( ",", devspec2array("TYPE=RESIDENTS,TYPE=ROOMMATE,TYPE=GUEST") ); +} + +# module Fn #################################################################### +sub HOMESTATEtk_InitializeDev($) { + my ($hash) = @_; + $hash = $defs{$hash} unless ( ref($hash) ); + my $name = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $changed = 0; + my $lang = + lc( AttrVal( $name, "Lang", AttrVal( "global", "language", "EN" ) ) ); + my $langUc = uc($lang); + my @error; + + RemoveInternalTimer($hash); + + no strict "refs"; + &{ $TYPE . "_Initialize" }( \%{ $modules{$TYPE} } ) if ($init_done); + use strict "refs"; + + # NOTIFYDEV + my $NOTIFYDEV = "global,$name"; + $NOTIFYDEV .= "," . HOMESTATEtk_findHomestateSlaves($hash); + unless ( defined( $hash->{NOTIFYDEV} ) && $hash->{NOTIFYDEV} eq $NOTIFYDEV ) + { + $hash->{NOTIFYDEV} = $NOTIFYDEV; + $changed = 1; + } + + my $time = time; + my $debugDate = AttrVal( $name, "DebugDate", "" ); + if ( $debugDate =~ + m/^((?:\d{4}\-)?\d{2}-\d{2})(?: (\d{2}:\d{2}(?::\d{2})?))?$/ ) + { + my $date = "$1"; + $date .= " $2" if ($2); + my ( $sec, $min, $hour, $mday, $mon, $year ) = UConv::_time(); + + $date = "$year-$date" unless ( $date =~ /^\d{4}-/ ); + $date .= "00:00:00" unless ( $date =~ /\d{2}:\d{2}:\d{2}$/ ); + $date .= ":00" unless ( $date =~ /\d{2}:\d{2}:\d{2}$/ ); + $time = time_str2num($date); + push @error, "WARNING: DebugDate in use ($date)"; + } + + delete $hash->{'.events'}; + delete $hash->{'.t'}; + $hash->{'.t'} = HOMESTATEtk_GetDaySchedule( $hash, $time, undef, $lang ); + $hash->{'.events'} = $hash->{'.t'}{events}; + + ## begin reading updates + # + readingsBeginUpdate($hash); + + foreach ( sort keys %{ $hash->{'.t'} } ) { + next if ( ref( $hash->{'.t'}{$_} ) ); + my $r = $readingsMap{$_} ? $readingsMap{$_} : undef; + my $v = $hash->{'.t'}{$_}; + + readingsBulkUpdateIfChanged( $hash, $r, $v ) + if ( defined($r) ); + + $r = $readingsMap_tom{$_} ? $readingsMap_tom{$_} : undef; + $v = $hash->{'.t'}{1}{$_} ? $hash->{'.t'}{1}{$_} : undef; + + readingsBulkUpdateIfChanged( $hash, $r, $v ) + if ( defined($r) && defined($v) ); + } + + unless ( $lang =~ /^en/i || !$hash->{'.t'}{$lang} ) { + foreach ( sort keys %{ $hash->{'.t'}{$lang} } ) { + next if ( ref( $hash->{'.t'}{$lang}{$_} ) ); + my $r = $readingsMap{$_} ? $readingsMap{$_} . "_$langUc" : undef; + my $v = $hash->{'.t'}{$lang}{$_}; + + readingsBulkUpdateIfChanged( $hash, $r, $v ) + if ( defined($r) ); + + $r = + $readingsMap_tom{$_} ? $readingsMap_tom{$_} . "_$langUc" : undef; + $v = + $hash->{'.t'}{1}{$lang}{$_} ? $hash->{'.t'}{1}{$lang}{$_} : undef; + + readingsBulkUpdateIfChanged( $hash, $r, $v ) + if ( defined($r) && defined($v) ); + } + } + + # TODO: seasonSocial + # + # TODO: höchster Sonnenstand + # > Trend Sonne aufgehend, abgehend? + # > temperaturmaximum 14h / 15h bei DST + + # error + if ( scalar @error ) { + readingsBulkUpdateIfChanged( $hash, "error", join( "; ", @error ) ); + } + else { + delete $hash->{READINGS}{error}; + } + + HOMESTATEtk_UpdateReadings($hash); + readingsEndUpdate( $hash, 1 ); + + # + ## end reading updates + + # schedule next timer + foreach ( sort keys %{ $hash->{'.events'} } ) { + next if ( $_ < $time ); + $hash->{NEXT_EVENT} = + $hash->{'.events'}{$_}{TIME} . " - " . $hash->{'.events'}{$_}{DESC}; + InternalTimer( $_, "HOMESTATEtk_InitializeDev", $hash ); + last; + } + + return 0 unless ($changed); + return undef; +} + +sub HOMESTATEtk_Define($$$) { + my ( $hash, $a, $h ) = @_; + my $name = shift @$a; + my $TYPE = shift @{$a}; + my $name_attr; + + $hash->{MOD_INIT} = 1; + $hash->{NOTIFYDEV} = "global"; + RemoveInternalTimer($hash); + + # set default settings on first define + if ( $init_done && !defined( $hash->{OLDDEF} ) ) { + HOMESTATEtk_Attr( "init", $name, "Lang" ); + + $attr{$name}{room} = "Homestate"; + $attr{$name}{devStateIcon} = + '{(HOMESTATEtk_devStateIcon($name),"toggle")}'; + + $attr{$name}{icon} = "control_building_control" + if ( $TYPE eq "HOMESTATE" ); + $attr{$name}{icon} = "control_building_eg" + if ( $TYPE eq "FLOORSTATE" ); + $attr{$name}{icon} = "floor" + if ( $TYPE eq "ROOMSTATE" ); + + # find HOMESTATE device + if ( $TYPE eq "ROOMSTATE" || $TYPE eq "FLOORSTATE" ) { + my @homestates = devspec2array("TYPE=HOMESTATE"); + if ( scalar @homestates ) { + $attr{$name}{"HomestateDevices"} = $homestates[0]; + $attr{$name}{room} = $attr{ $homestates[0] }{room} + if ( $attr{ $homestates[0] } + && $attr{ $homestates[0] }{room} ); + $attr{$name}{"Lang"} = $attr{ $homestates[0] }{Lang} + if ( $attr{ $homestates[0] } + && $attr{ $homestates[0] }{Lang} ); + + HOMESTATEtk_Attr( "set", $name, "Lang", $attr{$name}{"Lang"} ) + if $attr{$name}{"Lang"}; + } + else { + my $n = "Home"; + my $i = ""; + while ( IsDevice( $n . $i ) ) { + $i = 0 if ( $i eq "" ); + $i++; + } + CommandDefine( undef, "$n HOMESTATE" ); + $attr{$n}{comment} = + "Auto-created by $TYPE module for use with HOMESTATE Toolkit"; + $attr{$name}{"HomestateDevices"} = $n; + $attr{$name}{room} = $attr{$n}{room}; + $attr{$name}{"Lang"} = $attr{ $homestates[0] }{Lang} + if ( $attr{ $homestates[0] } + && $attr{ $homestates[0] }{Lang} ); + + HOMESTATEtk_Attr( "set", $name, "Lang", $attr{$name}{"Lang"} ) + if $attr{$name}{"Lang"}; + } + } + + # find ROOMSTATE device + if ( $TYPE eq "FLOORSTATE" ) { + my @roomstates = devspec2array("TYPE=ROOMSTATE"); + unless ( scalar @roomstates ) { + my $n = "Room"; + my $i = ""; + while ( IsDevice( $n . $i ) ) { + $i = 0 if ( $i eq "" ); + $i++; + } + CommandDefine( undef, "$n ROOMSTATE" ); + $attr{$n}{comment} = + "Auto-created by $TYPE module for use with HOMESTATE Toolkit"; + $attr{$name}{room} = $attr{$n}{room}; + $attr{$name}{"Lang"} = $attr{ $roomstates[0] }{Lang} + if ( $attr{ $roomstates[0] } + && $attr{ $roomstates[0] }{Lang} ); + + HOMESTATEtk_Attr( "set", $name, "Lang", $attr{$name}{"Lang"} ) + if $attr{$name}{"Lang"}; + } + } + + # find RESIDENTS device + if ( $TYPE eq "HOMESTATE" ) { + my @residents = devspec2array("TYPE=RESIDENTS,TYPE=ROOMMATE"); + if ( scalar @residents ) { + $attr{$name}{"ResidentsDevices"} = $residents[0]; + $attr{$name}{room} = $attr{ $residents[0] }{room} + if ( $attr{ $residents[0] } && $attr{ $residents[0] }{room} ); + $attr{$name}{"Lang"} = $attr{ $residents[0] }{rgr_lang} + if ( $attr{ $residents[0] } + && $attr{ $residents[0] }{rgr_lang} ); + $attr{$name}{"Lang"} = $attr{ $residents[0] }{rr_lang} + if ( $attr{ $residents[0] } + && $attr{ $residents[0] }{rr_lang} ); + + HOMESTATEtk_Attr( "set", $name, "Lang", $attr{$name}{"Lang"} ) + if $attr{$name}{"Lang"}; + } + else { + my $n = "rgr_Residents"; + my $i = ""; + while ( IsDevice( $n . $i ) ) { + $i = 0 if ( $i eq "" ); + $i++; + } + CommandDefine( undef, "$n RESIDENTS" ); + $attr{$n}{comment} = + "Auto-created by $TYPE module for use with HOMESTATE Toolkit"; + $attr{$name}{"ResidentsDevices"} = $n; + $attr{$n}{room} = $attr{$name}{room}; + $attr{$name}{"Lang"} = $attr{ $residents[0] }{rgr_lang} + if ( $attr{ $residents[0] } + && $attr{ $residents[0] }{rgr_lang} ); + $attr{$name}{"Lang"} = $attr{ $residents[0] }{rr_lang} + if ( $attr{ $residents[0] } + && $attr{ $residents[0] }{rr_lang} ); + + HOMESTATEtk_Attr( "set", $name, "Lang", $attr{$name}{"Lang"} ) + if $attr{$name}{"Lang"}; + + $attr{$name}{group} = $attr{$n}{group}; + } + } + } + + return undef; +} + +sub HOMESTATEtk_Undefine($$) { + my ( $hash, $name ) = @_; + RemoveInternalTimer($hash); + return undef; +} + +sub HOMESTATEtk_Set($$$); + +sub HOMESTATEtk_Set($$$) { + my ( $hash, $a, $h ) = @_; + my $TYPE = $hash->{TYPE}; + my $name = shift @$a; + my $lang = + lc( AttrVal( $name, "Lang", AttrVal( "global", "language", "EN" ) ) ); + my $langUc = uc($lang); + my $residents = ( $hash->{RESIDENTS} ? $hash->{RESIDENTS} : undef ); + my $state = ReadingsVal( $name, "state", "" ); + my $mode = ReadingsVal( $name, "mode", "" ); + my $autoMode = + HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "autoMode", "on" ), + $stateOnoff{en} ); + my $silent = 0; + my %rvals; + + return undef if ( IsDisabled($name) ); + return "No argument given" unless (@$a); + + my $cmd = shift @$a; + + my $usage = "toggle:noArg"; + my $usageL = ""; + + # usage: mode + my $i = + $autoMode && ReadingsVal( $name, "daytime", "night" ) ne "night" + ? HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "daytime", 0 ), + $UConv::daytimes{en} ) + : 0; + $usage .= " mode:"; + $usageL .= " mode_$langUc:"; + while ( $i < scalar @{ $UConv::daytimes{en} } ) { + last + if ( $autoMode + && $i == 6 + && ReadingsVal( $name, "daytime", "night" ) !~ + m/^evening|midevening|night$/ ); + if ( $autoMode + && ReadingsVal( $name, "daytime", "night" ) eq "night" + && $i > 3 + && $i != 6 ) + { + $i++; + } + else { + $usage .= $UConv::daytimes{en}[$i]; + $usageL .= $UConv::daytimes{$lang}[$i]; + $i++; + unless ( $autoMode + && $i == 6 + && ReadingsVal( $name, "daytime", "night" ) !~ + m/^evening|midevening|night$/ ) + { + $usage .= "," unless ( $i == scalar @{ $UConv::daytimes{en} } ); + $usageL .= "," + unless ( $i == scalar @{ $UConv::daytimes{$lang} } ); + } + } + } + + # usage: autoMode + $usage .= " autoMode:" . join( ",", @{ $stateOnoff{en} } ); + $usageL .= " autoMode_$langUc:" . join( ",", @{ $stateOnoff{$lang} } ); + + $usage .= " $usageL" unless ( $lang eq "en" ); + return "Set usage: choose one of $usage" + unless ( $cmd && $cmd ne "?" ); + + # mode + if ( $cmd eq "state" + || $cmd eq "mode" + || $cmd eq "state_$langUc" + || $cmd eq "mode_$langUc" + || grep ( m/^$cmd$/i, @{ $UConv::daytimes{en} } ) + || grep ( /^$cmd$/i, @{ $UConv::daytimes{$lang} } ) ) + { + $cmd = shift @$a + if ( $cmd eq "state" + || $cmd eq "mode" + || $cmd eq "state_$langUc" + || $cmd eq "mode_$langUc" ); + + my $i = HOMESTATEtk_GetIndexFromArray( $cmd, $UConv::daytimes{en} ); + $i = HOMESTATEtk_GetIndexFromArray( $cmd, $UConv::daytimes{$lang} ) + unless ( $lang eq "en" || defined($i) ); + $i = $cmd + if ( !defined($i) + && $cmd =~ /^\d+$/ + && defined( $UConv::daytimes{en}[$cmd] ) ); + + return "Invalid argument $cmd" + unless ( defined($i) ); + + my $id = + HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "daytime", 0 ), + $UConv::daytimes{en} ); + + if ($autoMode) { + + # during daytime, one cannot go back in time... + $i = $id + if ( ReadingsVal( $name, "daytime", "night" ) ne "night" + && $i < $id ); + + # evening is latest until evening itself was reached + $i = $id if ( $i >= 6 && $id <= 3 ); + + # afternoon is latest until morning was reached + $i = 6 if ( $i >= 4 && $i != 6 && $id == 6 ); + $i = 0 if ( $i > 6 && $id == 6 ); + } + + Log3 $name, 2, "$TYPE set $name mode " . $UConv::daytimes{en}[$i]; + $rvals{mode} = $UConv::daytimes{en}[$i]; + } + + # toggle + elsif ( $cmd eq "toggle" ) { + my $i = HOMESTATEtk_GetIndexFromArray( $mode, $UConv::daytimes{en} ); + my $id = + HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "daytime", 0 ), + $UConv::daytimes{en} ); + + $i++; + $i = 0 if ( $i == 7 ); + + if ($autoMode) { + + # during daytime, one cannot go back in time... + $i = $id + if ( ReadingsVal( $name, "daytime", "night" ) ne "night" + && $i < $id ); + + # evening is latest until evening itself was reached + $i = $id if ( $i >= 6 && $id <= 3 ); + + # afternoon is latest until morning was reached + $i = 6 if ( $i >= 4 && $i != 6 && $id == 6 ); + $i = 0 if ( $i > 6 && $id == 6 ); + } + + Log3 $name, 2, "$TYPE set $name mode " . $UConv::daytimes{en}[$i]; + $rvals{mode} = $UConv::daytimes{en}[$i]; + } + + # autoMode + elsif ( $cmd eq "autoMode" || $cmd eq "autoMode_$langUc" ) { + my $p1 = shift @$a; + + my $i = HOMESTATEtk_GetIndexFromArray( $p1, $stateOnoff{en} ); + $i = HOMESTATEtk_GetIndexFromArray( $p1, $stateOnoff{$lang} ) + unless ( $lang eq "en" || defined($i) ); + $i = $cmd + if ( !defined($i) + && $cmd =~ /^\d+$/ + && defined( $stateOnoff{en}[$cmd] ) ); + + return "Invalid argument $cmd" + unless ( defined($i) ); + + Log3 $name, 2, "$TYPE set $name autoMode " . $stateOnoff{en}[$i]; + $rvals{autoMode} = $stateOnoff{en}[$i]; + } + + # usage + else { + return "Unknown set command $cmd, choose one of $usage"; + } + + readingsBeginUpdate($hash); + + # if autoMode changed + if ( defined( $rvals{autoMode} ) ) { + readingsBulkUpdateIfChanged( $hash, "autoMode", $rvals{autoMode} ); + readingsBulkUpdateIfChanged( + $hash, + "autoMode_$langUc", + $stateOnoff{$lang}[ + HOMESTATEtk_GetIndexFromArray( + $rvals{autoMode}, $stateOnoff{en} + ) + ] + ) unless ( $lang eq "en" ); + + if ( $rvals{autoMode} eq "on" ) { + my $im = + HOMESTATEtk_GetIndexFromArray( $mode, $UConv::daytimes{en} ); + my $id = + HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "daytime", 0 ), + $UConv::daytimes{en} ); + + $rvals{mode} = $UConv::daytimes{en}[$id] + if ( $im < $id || ( $im == 6 && $id < 6 ) ) + ; #TODO check when switching during evening and midevening time + } + } + + # if mode changed + if ( defined( $rvals{mode} ) && $rvals{mode} ne $mode ) { + my $modeL = ReadingsVal( $name, "mode_$langUc", "" ); + readingsBulkUpdate( $hash, "lastMode", $mode ) if ( $mode ne "" ); + readingsBulkUpdate( $hash, "lastMode_$langUc", $modeL ) + if ( $modeL ne "" ); + $mode = $rvals{mode}; + $modeL = + $UConv::daytimes{$lang} + [ HOMESTATEtk_GetIndexFromArray( $rvals{mode}, $UConv::daytimes{en} ) + ]; + readingsBulkUpdate( $hash, "mode", $mode ); + readingsBulkUpdate( $hash, "mode_$langUc", $modeL ) + unless ( $lang eq "en" ); + } + + HOMESTATEtk_UpdateReadings($hash); + readingsEndUpdate( $hash, 1 ); + + return undef; +} + +sub HOMESTATEtk_Get($$$) { + my ( $hash, $a, $h ) = @_; + my $name = shift @$a; + my $lang = + lc( AttrVal( $name, "Lang", AttrVal( "global", "language", "EN" ) ) ); + my $langUc = uc($lang); + + return "No argument given" unless (@$a); + + my $cmd = shift @$a; + + my $usage = "Unknown argument $cmd, choose one of schedule"; + + # schedule + if ( $cmd eq "schedule" ) { + my $date = shift @$a; + my $time = shift @$a; + + if ($date) { + return "invalid date format $date" + unless ( !$date + || $date =~ m/^\d{4}\-\d{2}-\d{2}$/ + || $date =~ m/^\d{2}-\d{2}$/ ); + return "invalid time format $time" + unless ( !$time + || $time =~ m/^\d{2}:\d{2}(:\d{2})?$/ ); + + my ( $sec, $min, $hour, $mday, $mon, $year ) = UConv::_time(); + $date = "$year-$date" if ( $date =~ m/^\d{2}-\d{2}$/ ); + $time .= ":00" if ( $time && $time =~ m/^\d{2}:\d{2}$/ ); + + $date .= $time ? " $time" : " 00:00:00"; + + # ( $year, $mon, $mday, $hour, $min, $sec ) = + # split( /[\s.:-]+/, $date ); + # $date = timelocal( $sec, $min, $hour, $mday, $mon - 1, $year ); + $date = time_str2num($date); + } + + #TODO timelocal? 03-26 results in wrong timestamp + # return PrintHash( + # $date + # ? %{ HOMESTATEtk_GetDaySchedule( $hash, $date, undef, $lang ) } + # {events} + # : $hash->{helper}{events}, + # 3 + # ); + return PrintHash( + $date + ? HOMESTATEtk_GetDaySchedule( $hash, $date, undef, $lang ) + : $hash->{'.events'}, + 3 + ); + } + + # return usage hint + else { + return $usage; + } + + return undef; +} + +sub HOMESTATEtk_Attr(@) { + my ( $cmd, $name, $attribute, $value ) = @_; + my $hash = $defs{$name}; + my $TYPE = $hash->{TYPE}; + + if ( $attribute eq "HomestateDevices" ) { + return "Value for $attribute has invalid format" + unless ( $cmd eq "del" + || $value =~ m/^[A-Za-z\d._]+(?:,[A-Za-z\d._]*)*$/ ); + + delete $hash->{HOMESTATES}; + $hash->{HOMESTATES} = $value unless ( $cmd eq "del" ); + } + + elsif ( $attribute eq "FloorstateDevices" ) { + return "Value for $attribute has invalid format" + unless ( $cmd eq "del" + || $value =~ m/^[A-Za-z\d._]+(?:,[A-Za-z\d._]*)*$/ ); + + delete $hash->{FLOORSTATES}; + $hash->{FLOORSTATES} = $value unless ( $cmd eq "del" ); + } + + elsif ( $attribute eq "RoomstateDevices" ) { + return "Value for $attribute has invalid format" + unless ( $cmd eq "del" + || $value =~ m/^[A-Za-z\d._]+(?:,[A-Za-z\d._]*)*$/ ); + + delete $hash->{ROOMSTATES}; + $hash->{ROOMSTATES} = $value unless ( $cmd eq "del" ); + } + + elsif ( $attribute eq "ResidentsDevices" ) { + return "Value for $attribute has invalid format" + unless ( $cmd eq "del" + || $value =~ m/^[A-Za-z\d._]+(?:,[A-Za-z\d._]*)*$/ ); + + delete $hash->{RESIDENTS}; + $hash->{RESIDENTS} = $value unless ( $cmd eq "del" ); + } + + elsif ( $attribute eq "HolidayDevices" ) { + return "Value for $attribute has invalid format" + unless ( $cmd eq "del" + || $value =~ m/^[A-Za-z\d._]+(?:,[A-Za-z\d._]*)*$/ ); + } + + elsif ( $attribute eq "DebugDate" ) { + return + "Invalid format for $attribute. Can be:\n" + . "\nYYYY-MM-DD" + . "\nYYYY-MM-DD HH:MM" + . "\nYYYY-MM-DD HH:MM:SS" + . "\nMM-DD" + . "\nMM-DD HH:MM" + . "\nMM-DD HH:MM:SS" + unless ( $cmd eq "del" + || $value =~ + m/^((?:\d{4}\-)?\d{2}-\d{2})(?: (\d{2}:\d{2}(?::\d{2})?))?$/ ); + } + + elsif ( !$init_done ) { + return undef; + } + + elsif ( $attribute eq "disable" ) { + if ( $value and $value == 1 ) { + $hash->{STATE} = "disabled"; + } + elsif ( $cmd eq "del" or !$value ) { + evalStateFormat($hash); + } + } + + elsif ( $attribute eq "Lang" ) { + my $lang = + $cmd eq "set" + ? lc($value) + : lc( AttrVal( "global", "language", "EN" ) ); + my $langUc = uc($lang); + + # for initial define, ensure fallback to EN + $lang = "en" + if ( $cmd eq "init" && $lang !~ /^en|de$/i ); + + if ( $lang eq "de" ) { + $attr{$name}{alias} = "Modus" + if ( !defined( $attr{$name}{alias} ) + || $attr{$name}{alias} eq "Mode" ); + $attr{$name}{webCmd} = "mode_$langUc" + if ( !defined( $attr{$name}{webCmd} ) + || $attr{$name}{webCmd} eq "mode" ); + + if ( $TYPE eq "HOMESTATE" ) { + $attr{$name}{group} = "Zuhause Status" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Home State" ); + } + if ( $TYPE eq "FLOORSTATE" ) { + $attr{$name}{group} = "Flurstatus" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Floor State" ); + } + if ( $TYPE eq "ROOMSTATE" ) { + $attr{$name}{group} = "Raumstatus" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Room State" ); + } + } + + elsif ( $lang eq "en" ) { + $attr{$name}{alias} = "Mode" + if ( !defined( $attr{$name}{alias} ) + || $attr{$name}{alias} eq "Modus" ); + $attr{$name}{webCmd} = "mode" + if ( !defined( $attr{$name}{webCmd} ) + || $attr{$name}{webCmd} =~ /^mode_[A-Z]{2}$/ ); + + if ( $TYPE eq "HOMESTATE" ) { + $attr{$name}{group} = "Home State" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Zuhause Status" ); + } + if ( $TYPE eq "FLOORSTATE" ) { + $attr{$name}{group} = "Floor State" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Flurstatus" ); + } + if ( $TYPE eq "ROOMSTATE" ) { + $attr{$name}{group} = "Room State" + if ( !defined( $attr{$name}{group} ) + || $attr{$name}{group} eq "Raumstatus" ); + } + } + else { + return "Unsupported language $langUc"; + } + + $attr{$name}{$attribute} = $value if ( $cmd eq "set" ); + evalStateFormat($hash); + } + + return undef; +} + +sub HOMESTATEtk_Notify($$) { + my ( $hash, $dev ) = @_; + my $name = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $devName = $dev->{NAME}; + my $devPrefix = RESIDENTStk_GetPrefixFromType($devName); + my $devType = GetType($devName); + + if ( $devName eq "global" ) { + my $events = deviceEvents( $dev, 1 ); + return "" unless ($events); + + foreach ( @{$events} ) { + + next if ( $_ =~ m/^[A-Za-z\d_-]+:/ ); + + # module and device initialization + if ( $_ =~ m/^INITIALIZED|REREADCFG$/ ) { + if ( !defined( &{'DoInitDev'} ) ) { + if ( $_ eq "REREADCFG" ) { + delete $modules{$devType}{READY}; + delete $modules{$devType}{INIT}; + } + RESIDENTStk_DoInitDev( + devspec2array("TYPE=$TYPE:FILTER=MOD_INIT=.+") ); + } + } + + # if any of our monitored devices was modified, + # recalculate monitoring status + elsif ( $_ =~ + m/^(DEFINED|MODIFIED|RENAMED|DELETED)\s+([A-Za-z\d_-]+)$/ ) + { + if ( defined( &{'DoInitDev'} ) ) { + + # DELETED would normally be handled by fhem.pl and imply + # DoModuleTrigger instead of DoInitDev to update module + # init state + next if ( $_ =~ /^DELETED/ ); + DoInitDev($name); + } + else { + + # for DELETED, we normally would want to use + # DoModuleTrigger() but we miss the deleted + # device's TYPE at this state :-( + RESIDENTStk_DoInitDev($name); + } + } + + # only process attribute events + next + unless ( $_ =~ +m/^((?:DELETE)?ATTR)\s+([A-Za-z\d._]+)\s+([A-Za-z\d_\.\-\/]+)(?:\s+(.*)\s*)?$/ + ); + + my $cmd = $1; + my $d = $2; + my $attr = $3; + my $val = $4; + my $type = GetType($d); + + # filter attributes to be processed + next + unless ( $attr =~ /[Dd]evices?$/ + || $attr eq "DebugDate" ); + + # when own attributes were changed + if ( $d eq $name ) { + if ( defined( &{'DoInitDev'} ) ) { + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 0.5, "DoInitDev", $hash ); + } + else { + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 0.5, + "RESIDENTStk_DoInitDev", $hash ); + } + return ""; + } + } + + return ""; + } + + return "" if ( IsDisabled($name) or IsDisabled($devName) ); + + # process events from RESIDENTS, ROOMMATE or GUEST devices + # only when they hit HOMESTATE devices + if ( $TYPE ne $devType + && $devType =~ + m/^HOMESTATE|FLOORSTATE|ROOMSTATE|RESIDENTS|ROOMMATE|GUEST$/ ) + { + + my $events = deviceEvents( $dev, 1 ); + return "" unless ($events); + + foreach my $event ( @{$events} ) { + next unless ( defined($event) ); + + # state changed + if ( $event !~ /^[a-zA-Z\d._]+:/ + || $event =~ /^state:/ + || $event =~ /^presence:/ + || $event =~ /^wayhome:/ + || $event =~ /^wakeup:/ ) + { + if ( defined( &{'DoInitDev'} ) ) { + DoInitDev($name); + } + else { + RESIDENTStk_DoInitDev($name); + } + } + } + + return ""; + } + + # process own events + elsif ( $devName eq $name ) { + my $events = deviceEvents( $dev, 1 ); + return "" unless ($events); + + foreach my $event ( @{$events} ) { + next unless ( defined($event) ); + + } + + return ""; + } + + return ""; +} + +sub HOMESTATEtk_GetIndexFromArray($$) { + my ( $string, $array ) = @_; + return undef unless ( ref($array) eq "ARRAY" ); + my ($index) = grep { $array->[$_] =~ /^$string$/i } ( 0 .. @$array - 1 ); + return defined $index ? $index : undef; +} + +sub HOMESTATEtk_findHomestateSlaves($;$) { + my ( $hash, $ret ) = @_; + my @slaves; + + if ( $hash->{TYPE} eq "HOMESTATE" ) { + + my @FLOORSTATES; + foreach ( devspec2array("TYPE=FLOORSTATE") ) { + next + unless ( + defined( $defs{$_}{FLOORSTATES} ) + && grep { $hash->{NAME} eq $_ } + split( /,/, $defs{$_}{FLOORSTATES} ) + ); + push @FLOORSTATES, $_; + } + + if ( scalar @FLOORSTATES ) { + $hash->{FLOORSTATES} = join( ",", @FLOORSTATES ); + } + elsif ( $hash->{FLOORSTATES} ) { + delete $hash->{FLOORSTATES}; + } + + if ( $hash->{FLOORSTATES} ) { + $ret .= "," if ($ret); + $ret .= $hash->{FLOORSTATES}; + } + } + + if ( $hash->{TYPE} eq "HOMESTATE" || $hash->{TYPE} eq "FLOORSTATE" ) { + + my @ROOMSTATES; + foreach ( devspec2array("TYPE=ROOMSTATE") ) { + next + unless ( + ( + defined( $defs{$_}{HOMESTATES} ) + && grep { $hash->{NAME} eq $_ } + split( /,/, $defs{$_}{HOMESTATES} ) + ) + || ( + defined( $defs{$_}{FLOORSTATES} ) + && grep { $hash->{NAME} eq $_ } + split( /,/, $defs{$_}{FLOORSTATES} ) + ) + ); + push @ROOMSTATES, $_; + } + + if ( scalar @ROOMSTATES ) { + $hash->{ROOMSTATES} = join( ",", @ROOMSTATES ); + } + elsif ( $hash->{ROOMSTATES} ) { + delete $hash->{ROOMSTATES}; + } + + if ( $hash->{ROOMSTATES} ) { + $ret .= "," if ($ret); + $ret .= $hash->{ROOMSTATES}; + } + } + + if ( $hash->{RESIDENTS} ) { + $ret .= "," if ($ret); + $ret .= $hash->{RESIDENTS}; + } + + return HOMESTATEtk_findDummySlaves( $hash, $ret ); +} + +sub HOMESTATEtk_findDummySlaves($;$); + +sub HOMESTATEtk_findDummySlaves($;$) { + my ( $hash, $ret ) = @_; + $ret = "" unless ($ret); + + return $ret; +} + +sub HOMESTATEtk_devStateIcon($) { + my ($hash) = @_; + $hash = $defs{$hash} if ( ref($hash) ne 'HASH' ); + + return undef if ( !$hash ); + my $name = $hash->{NAME}; + my $lang = + lc( AttrVal( $name, "Lang", AttrVal( "global", "language", "EN" ) ) ); + my $langUc = uc($lang); + my @devStateIcon; + + # mode + my $i = 0; + foreach ( @{ $UConv::daytimes{en} } ) { + push @devStateIcon, "$_:$UConv::daytimes{icons}[$i++]:toggle"; + } + unless ( $lang eq "en" && defined( $UConv::daytimes{$lang} ) ) { + $i = 0; + foreach ( @{ $UConv::daytimes{$lang} } ) { + push @devStateIcon, "$_:$UConv::daytimes{icons}[$i++]:toggle"; + } + } + + # security + $i = 0; + foreach ( @{ $stateSecurity{en} } ) { + push @devStateIcon, "$_:$stateSecurity{icons}[$i++]"; + } + unless ( $lang eq "en" && defined( $UConv::daytimes{$lang} ) ) { + $i = 0; + foreach ( @{ $stateSecurity{$lang} } ) { + push @devStateIcon, "$_:$stateSecurity{icons}[$i++]"; + } + } + + return join( " ", @devStateIcon ); +} + +sub HOMESTATEtk_GetDaySchedule($;$$$$$) { + my ( $hash, $time, $totalTemporalHours, $lang, @srParams ) = @_; + my $name = $hash->{NAME}; + $lang = ( + $attr{global}{language} + ? $attr{global}{language} + : "EN" + ) unless ($lang); + + return undef + unless ( !$time || $time =~ /^\d{10}(?:\.\d+)?$/ ); + + my $ret = UConv::GetDaytime( $time, $totalTemporalHours, $lang, @srParams ); + + # consider user defined vacation days + my $holidayDevs = AttrVal( $name, "HolidayDevices", "" ); + foreach my $holidayDev ( split( /,/, $holidayDevs ) ) { + next + unless ( IsDevice( $holidayDev, "holiday" ) + && AttVal( "global", "holiday2we", "" ) ne $holidayDev ); + + my $date = sprintf( "%02d-%02d", $ret->{monISO}, $ret->{mday} ); + my $tod = holiday_refresh( $holidayDev, $date ); + $date = + sprintf( "%02d-%02d", $ret->{'-1'}{monISO}, $ret->{'-1'}{mday} ); + my $ytd = holiday_refresh( $holidayDev, $date ); + $date = sprintf( "%02d-%02d", $ret->{1}{monISO}, $ret->{1}{mday} ); + my $tom = holiday_refresh( $holidayDev, $date ); + + if ( $tod ne "none" ) { + $ret->{iswe} += 3; + $ret->{isholiday} += 2; + $ret->{day_desc} = $tod unless ( $ret->{isholiday} == 3 ); + $ret->{day_desc} .= ", $tod" if ( $ret->{isholiday} == 3 ); + } + if ( $ytd ne "none" ) { + $ret->{'-1'}{isholiday} += 2; + $ret->{'-1'}{day_desc} = $ytd + unless ( $ret->{'-1'}{isholiday} == 3 ); + $ret->{'-1'}{day_desc} .= ", $ytd" + if ( $ret->{'-1'}{isholiday} == 3 ); + } + if ( $tom ne "none" ) { + $ret->{1}{isholiday} += 2; + $ret->{1}{day_desc} = $tom; + $ret->{1}{day_desc} = $tom unless ( $ret->{1}{isholiday} == 3 ); + $ret->{1}{day_desc} .= ", $tom" + if ( $ret->{1}{isholiday} == 3 ); + } + } + + return $ret; +} + +sub HOMESTATEtk_UpdateReadings (@) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $TYPE = $hash->{TYPE}; + my $t = $hash->{'.t'}; + my $state = ReadingsVal( $name, "state", "" ); + my $security = ReadingsVal( $name, "security", "" ); + my $daytime = ReadingsVal( $name, "daytime", "" ); + my $mode = ReadingsVal( $name, "mode", "" ); + my $autoMode = + HOMESTATEtk_GetIndexFromArray( ReadingsVal( $name, "autoMode", "on" ), + $stateOnoff{en} ); + my $lang = + lc( AttrVal( $name, "Lang", AttrVal( "global", "language", "EN" ) ) ); + my $langUc = uc($lang); + + # presence + my $state_home = 0; + my $state_gotosleep = 0; + my $state_asleep = 0; + my $state_awoken = 0; + my $state_absent = 0; + my $state_gone = 0; + my $wayhome = 0; + my $wayhomeDelayed = 0; + my $wakeup = 0; + foreach my $internal ( "RESIDENTS", "FLOORSTATES", "ROOMSTATES" ) { + next unless ( $hash->{$internal} ); + foreach my $presenceDev ( split( /,/, $hash->{$internal} ) ) { + my $state = ReadingsVal( $presenceDev, "state", "gone" ); + $state_home++ if ( $state eq "home" ); + $state_gotosleep++ if ( $state eq "gotosleep" ); + $state_asleep++ if ( $state eq "asleep" ); + $state_awoken++ if ( $state eq "awoken" ); + $state_absent++ if ( $state eq "absent" ); + $state_gone++ if ( $state eq "gone" || $state eq "none" ); + + my $wayhome = ReadingsVal( $presenceDev, "wayhome", 0 ); + $wayhome++ if ($wayhome); + $wayhomeDelayed++ if ( $wayhome == 2 ); + + my $wakeup = ReadingsVal( $presenceDev, "wakeup", 0 ); + $wakeup++ if ($wakeup); + } + } + $state_home = 1 + unless ( $hash->{RESIDENTS} + || $hash->{FLOORSTATES} + || $hash->{ROOMSTATES} ); + + # autoMode + if ( $autoMode && $mode ne $daytime ) { + my $im = HOMESTATEtk_GetIndexFromArray( $mode, $UConv::daytimes{en} ); + my $id = + HOMESTATEtk_GetIndexFromArray( $daytime, $UConv::daytimes{en} ); + + if ( + $mode eq "" || ( + + # follow daymode throughout the day until midnight + ( $im < $id && $id != 6 ) + + # first change after midnight + || ( $im >= 4 && $id == 6 ) + + # morning + || ( $im == 6 && $id >= 0 && $id <= 3 ) + ) + ) + { + readingsBulkUpdate( $hash, "lastMode", $mode ) if ( $mode ne "" ); + $mode = $daytime; + readingsBulkUpdate( $hash, "mode", $mode ); + unless ( $lang eq "en" ) { + my $modeL = ReadingsVal( $name, "mode_$langUc", "" ); + my $daytimeL = ReadingsVal( $name, "daytime_$langUc", "" ); + readingsBulkUpdate( $hash, "lastMode_$langUc", $modeL ) + if ( $modeL ne "" ); + readingsBulkUpdate( $hash, "mode_$langUc", $daytimeL ) + if ( $daytimeL ne "" ); + } + } + } + + # + # security calculation + # + my $newsecurity; + + # unsecured + if ( $state_home > 0 && $mode !~ /^night|midevening$/ ) { + $newsecurity = "unlocked"; + } + + # locked + elsif ($state_home > 0 + || $state_awoken > 0 + || $state_gotosleep > 0 + || $wakeup > 0 ) + { + $newsecurity = "locked"; + } + + # night + elsif ( $state_asleep > 0 ) { + $newsecurity = "protected"; + } + + # secured + elsif ( $state_absent > 0 || $wayhome > 0 ) { + $newsecurity = "secured"; + } + + # extended + else { + $newsecurity = "guarded"; + } + + if ( $newsecurity ne $security ) { + readingsBulkUpdate( $hash, "lastSecurity", $security ) + if ( $security ne "" ); + $security = $newsecurity; + readingsBulkUpdate( $hash, "security", $security ); + + unless ( $lang eq "en" ) { + my $securityL = ReadingsVal( $name, "security_$langUc", "" ); + readingsBulkUpdate( $hash, "lastSecurity_$langUc", $securityL ) + if ( $securityL ne "" ); + $securityL = + $stateSecurity{$lang} + [ HOMESTATEtk_GetIndexFromArray( $security, $stateSecurity{en} ) + ]; + readingsBulkUpdate( $hash, "security_$langUc", $securityL ); + } + } + + # + # state calculation: + # combine security and mode + # + my $newstate; + my $statesrc; + + # mode + if ( $security =~ m/^unlocked|locked/ ) { + $newstate = $mode; + $statesrc = "mode"; + } + + # security + else { + $newstate = $security; + $statesrc = "security"; + } + + if ( $newstate ne $state ) { + readingsBulkUpdate( $hash, "lastState", $state ) if ( $state ne "" ); + $state = $newstate; + readingsBulkUpdate( $hash, "state", $state ); + + unless ( $lang eq "en" ) { + my $stateL = ReadingsVal( $name, "state_$langUc", "" ); + readingsBulkUpdate( $hash, "lastState_$langUc", $stateL ) + if ( $stateL ne "" ); + $stateL = ReadingsVal( $name, $statesrc . "_$langUc", "" ); + readingsBulkUpdate( $hash, "state_$langUc", $stateL ); + } + } + +} + +1; + diff --git a/fhem/FHEM/UConv.pm b/fhem/FHEM/UConv.pm index fd37ddc91..4386cdbfe 100644 --- a/fhem/FHEM/UConv.pm +++ b/fhem/FHEM/UConv.pm @@ -1242,10 +1242,13 @@ sub GetDaytime(;$$$$) { # Midnight $ret->{events}{ $ret->{midnight_t} }{TIME} = main::FmtDateTime( $ret->{midnight_t} ); - $ret->{events}{ $ret->{midnight_t} }{DESC} = "Begin new calendar day"; + $ret->{events}{ $ret->{midnight_t} }{DESC} = + "Begin night time and new calendar day"; $ret->{events}{ $ret->{1}{midnight_t} }{TIME} = main::FmtDateTime( $ret->{1}{midnight_t} ); - $ret->{events}{ $ret->{1}{midnight_t} }{DESC} = "Begin new calendar day"; + $ret->{events}{ $ret->{1}{midnight_t} }{TIME} =~ s/00:00:00/24:00:00/; + $ret->{events}{ $ret->{1}{midnight_t} }{DESC} = + "End calendar day and begin night time"; # Holidays $ret->{events}{ $ret->{midnight_t} }{DESC} .= @@ -1287,7 +1290,7 @@ sub GetDaytime(;$$$$) { my $t = int( $b + 0.5 ); $ret->{events}{$t}{TIME} = main::FmtDateTime($t); if ( $i == $ret->{daytimeStages} + 1 ) { - $ret->{events}{$t}{DESC} = "Begin nighttime"; + $ret->{events}{$t}{DESC} = "Begin midevening time"; } else { $ret->{events}{$t}{DESC} = "Begin daytime stage $i" @@ -1821,10 +1824,6 @@ sub _time(;$$$) { } sub _GetIndexFromArray($$) { - - # my ( $string, @array ) = @_; - # my ($index) = grep { $array[$_] =~ /^$string$/i } ( 0 .. @array - 1 ); - # return defined $index ? $index : undef; my ( $string, $array ) = @_; return undef unless ( ref($array) eq "ARRAY" ); my ($index) = grep { $array->[$_] =~ /^$string$/i } ( 0 .. @$array - 1 ); diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 444dbc0ad..6c7f76e2b 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -445,6 +445,7 @@ FHEM/Color.pm justme1968 http://forum.fhem.de Sonstiges FHEM/FritzBoxUtils.pm rudolfkoenig http://forum.fhem.de FRITZ!Box FHEM/HMCCUConf.pm zap http://forum.fhem.de HomeMatic FHEM/HMConfig.pm martinp876 http://forum.fhem.de HomeMatic +FHEM/HOMESTATEtk.pm loredo http://forum.fhem.de Automatisierung FHEM/HttpUtils.pm rudolfkoenig http://forum.fhem.de Automatisierung FHEM/MaxCommon.pm rudolfkoenig/orphan http://forum.fhem.de MAX FHEM/msgSchema.pm loredo http://forum.fhem.de Automatisierung