############################################################################### # $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@yellow', 'status_standby@yellow@green', 'status_night@green', 'status_locked@green', 'building_security@green' ], ); 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; delete $hash->{NEXT_EVENT}; 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"; delete $hash->{NEXT_EVENT}; 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 "SECTIONSTATE" ); $attr{$name}{icon} = "floor" if ( $TYPE eq "ROOMSTATE" ); # find HOMESTATE device if ( $TYPE eq "ROOMSTATE" || $TYPE eq "SECTIONSTATE" ) { 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 "SECTIONSTATE" ) { 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}; 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 ) = @_; delete $hash->{NEXT_EVENT}; 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 $state = ReadingsVal( $name, "state", "" ); my $mode = ReadingsVal( $name, "mode", "" ); my $security = ReadingsVal( $name, "security", "" ); 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 "?" ); return "Device is currently $security and cannot be controlled at this state" unless ( $security =~ m/^unlocked|locked$/ ); # 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}$/ || $date =~ m/^\d{10}$/ ); return "invalid time format $time" unless ( !$time || $time =~ m/^\d{2}:\d{2}(:\d{2})?$/ ); unless ( $date =~ m/^\d{10}$/ ) { 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}; my $security = ReadingsVal( $name, "security", "" ); return "Device is currently $security and attributes cannot be changed at this state" unless ( !$init_done || $security =~ m/^unlocked|locked$/ ); 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->{SECTIONSTATES}; $hash->{SECTIONSTATES} = $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 "SECTIONSTATE" ) { $attr{$name}{group} = "Bereichstatus" if ( !defined( $attr{$name}{group} ) || $attr{$name}{group} eq "Section 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 "SECTIONSTATE" ) { $attr{$name}{group} = "Section State" if ( !defined( $attr{$name}{group} ) || $attr{$name}{group} eq "Bereichstatus" ); } 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 $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'} ) ) { delete $hash->{NEXT_EVENT}; RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 0.5, "DoInitDev", $hash ); } else { delete $hash->{NEXT_EVENT}; 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|SECTIONSTATE|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 =~ /^mode:/ || $event =~ /^security:/ || $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 @SECTIONSTATES; foreach ( devspec2array("TYPE=SECTIONSTATE") ) { next unless ( defined( $defs{$_}{SECTIONSTATES} ) && grep { $hash->{NAME} eq $_ } split( /,/, $defs{$_}{SECTIONSTATES} ) ); push @SECTIONSTATES, $_; } if ( scalar @SECTIONSTATES ) { $hash->{SECTIONSTATES} = join( ",", @SECTIONSTATES ); } elsif ( $hash->{SECTIONSTATES} ) { delete $hash->{SECTIONSTATES}; } if ( $hash->{SECTIONSTATES} ) { $ret .= "," if ($ret); $ret .= $hash->{SECTIONSTATES}; } } if ( $hash->{TYPE} eq "HOMESTATE" || $hash->{TYPE} eq "SECTIONSTATE" ) { my @ROOMSTATES; foreach ( devspec2array("TYPE=ROOMSTATE") ) { next unless ( ( defined( $defs{$_}{HOMESTATES} ) && grep { $hash->{NAME} eq $_ } split( /,/, $defs{$_}{HOMESTATES} ) ) || ( defined( $defs{$_}{SECTIONSTATES} ) && grep { $hash->{NAME} eq $_ } split( /,/, $defs{$_}{SECTIONSTATES} ) ) ); 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" ) && AttrVal( "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", "SECTIONSTATES", "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->{SECTIONSTATES} || $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;