# $Id$ ############################################################################## # # 20_GUEST.pm # Submodule of 10_RESIDENTS. # # Copyright by Julian Pawlowski # e-mail: julian.pawlowski at gmail.com # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; use Time::Local; use Data::Dumper; require RESIDENTStk; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; sub GUEST_Set($@); sub GUEST_Define($$); sub GUEST_Notify($$); sub GUEST_Undefine($$); ################################### sub GUEST_Initialize($) { my ($hash) = @_; Log3 $hash, 5, "GUEST_Initialize: Entering"; $hash->{SetFn} = "GUEST_Set"; $hash->{DefFn} = "GUEST_Define"; $hash->{NotifyFn} = "GUEST_Notify"; $hash->{UndefFn} = "GUEST_Undefine"; $hash->{AttrList} = "rg_locationHome rg_locationWayhome rg_locationUnderway rg_autoGoneAfter:12,16,24,26,28,30,36,48,60 rg_showAllStates:0,1 rg_realname:group,alias rg_states:multiple-strict,home,gotosleep,asleep,awoken,absent,gone rg_locations rg_moods rg_moodDefault rg_moodSleepy rg_noDuration:0,1 rg_wakeupDevice rg_geofenceUUIDs " . $readingFnAttributes; } ################################### sub GUEST_Define($$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); my $name = $hash->{NAME}; my $name_attr; Log3 $name, 5, "GUEST $name: called function GUEST_Define()"; if ( int(@a) < 2 ) { my $msg = "Wrong syntax: define GUEST [RESIDENTS-DEVICE-NAMES]"; Log3 $name, 4, $msg; return $msg; } $hash->{TYPE} = "GUEST"; my $parents = ( defined( $a[2] ) ? $a[2] : "" ); # unregister at parent objects if we get modified my @registeredResidentgroups; my $modified = 0; if ( defined( $hash->{RESIDENTGROUPS} ) && $hash->{RESIDENTGROUPS} ne "" ) { $modified = 1; @registeredResidentgroups = split( /,/, $hash->{RESIDENTGROUPS} ); # unregister at parent objects foreach my $parent (@registeredResidentgroups) { if ( defined( $defs{$parent} ) && $defs{$parent}{TYPE} eq "RESIDENTS" ) { fhem("set $parent unregister $name"); Log3 $name, 4, "GUEST $name: Unregistered at RESIDENTS device $parent"; } } } # register at parent objects $hash->{RESIDENTGROUPS} = ""; if ( $parents ne "" ) { @registeredResidentgroups = split( /,/, $parents ); foreach my $parent (@registeredResidentgroups) { if ( !defined( $defs{$parent} ) ) { Log3 $name, 3, "GUEST $name: Unable to register at RESIDENTS device $parent (not existing)"; next; } if ( $defs{$parent}{TYPE} ne "RESIDENTS" ) { Log3 $name, 3, "GUEST $name: Device $parent is not a RESIDENTS device (wrong type)"; next; } fhem("set $parent register $name"); $hash->{RESIDENTGROUPS} .= $parent . ","; Log3 $name, 4, "GUEST $name: Registered at RESIDENTS device $parent"; } } else { $modified = 0; } # set reverse pointer $modules{GUEST}{defptr}{$name} = \$hash; readingsBeginUpdate($hash); # set default settings on first define if ($init_done) { my $aliasname = $name; $aliasname =~ s/^rg_//; $attr{$name}{alias} = $aliasname; $attr{$name}{devStateIcon} = ".*home:user_available:absent .*absent:user_away:home .*none:control_building_empty:home .*gotosleep:scene_toilet:asleep .*asleep:scene_sleeping:awoken .*awoken:scene_sleeping_alternat:home .*:user_unknown:home"; $attr{$name}{group} = "Guests"; $attr{$name}{icon} = "scene_visit_guests"; $attr{$name}{rg_realname} = "alias"; $attr{$name}{sortby} = "1"; $attr{$name}{webCmd} = "state"; $attr{$name}{room} = $attr{ $registeredResidentgroups[0] }{room} if ( @registeredResidentgroups && exists( $attr{ $registeredResidentgroups[0] }{room} ) ); } # trigger for modified objects unless ( $modified == 0 ) { readingsBulkUpdate( $hash, "state", ReadingsVal( $name, "state", "" ) ); } readingsEndUpdate( $hash, 1 ); # run timers InternalTimer( gettimeofday() + 15, "GUEST_StartInternalTimers", $hash, 0 ); # Injecting AttrFn for use with RESIDENTS Toolkit if ( !defined( $modules{dummy}{AttrFn} ) ) { $modules{dummy}{AttrFn} = "RESIDENTStk_AttrFnDummy"; } elsif ( $modules{dummy}{AttrFn} ne "RESIDENTStk_AttrFnDummy" ) { Log3 $name, 4, "RESIDENTStk $name: concurrent AttrFn already defined for dummy module (" . $modules{dummy}{AttrFn} . "). Some attribute based functions like auto-creations will not be available."; } return undef; } ################################### sub GUEST_Undefine($$) { my ( $hash, $name ) = @_; RESIDENTStk_RemoveInternalTimer( "AutoGone", $hash ); RESIDENTStk_RemoveInternalTimer( "DurationTimer", $hash ); if ( defined( $hash->{RESIDENTGROUPS} ) ) { my @registeredResidentgroups = split( /,/, $hash->{RESIDENTGROUPS} ); # unregister at parent objects foreach my $parent (@registeredResidentgroups) { if ( defined( $defs{$parent} ) && $defs{$parent}{TYPE} eq "RESIDENTS" ) { fhem("set $parent unregister $name"); Log3 $name, 4, "GUEST $name: Unregistered at RESIDENTS device $parent"; } } } # release reverse pointer delete $modules{GUEST}{defptr}{$name}; return undef; } ################################### sub GUEST_Notify($$) { my ( $hash, $dev ) = @_; my $devName = $dev->{NAME}; my $hashName = $hash->{NAME}; # process child notifies if ( $devName ne $hashName ) { my @registeredWakeupdevs = split( /,/, $attr{$hashName}{rg_wakeupDevice} ) if ( defined( $attr{$hashName}{rg_wakeupDevice} ) && $attr{$hashName}{rg_wakeupDevice} ne "" ); # if we have registered wakeup devices if (@registeredWakeupdevs) { # if this is a notification of a registered wakeup device if ( $devName ~~ @registeredWakeupdevs ) { # Some previous notify deleted the array. return if ( !$dev->{CHANGED} ); foreach my $change ( @{ $dev->{CHANGED} } ) { RESIDENTStk_wakeupSet( $devName, $change ); } return; } # process sub-child notifies: *_wakeupDevice foreach my $wakeupDev (@registeredWakeupdevs) { # if this is a notification of a registered sub dummy device # of one of our wakeup devices if ( defined( $attr{$wakeupDev}{wakeupResetSwitcher} ) && $attr{$wakeupDev}{wakeupResetSwitcher} eq $devName && $defs{$devName}{TYPE} eq "dummy" ) { # Some previous notify deleted the array. return if ( !$dev->{CHANGED} ); foreach my $change ( @{ $dev->{CHANGED} } ) { RESIDENTStk_wakeupSet( $wakeupDev, $change ) if ( $change ne "off" ); } last; } } } } return; } ################################### sub GUEST_Set($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $state = ReadingsVal( $name, "state", "initialized" ); my $presence = ReadingsVal( $name, "presence", "undefined" ); my $mood = ReadingsVal( $name, "mood", "-" ); my $location = ReadingsVal( $name, "location", "undefined" ); my $silent = 0; Log3 $name, 5, "GUEST $name: called function GUEST_Set()"; return "No Argument given" if ( !defined( $a[1] ) ); # depending on current FHEMWEB instance's allowedCommands, # restrict set commands if there is "set-user" in it my $adminMode = 1; my $FWallowedCommands = 0; $FWallowedCommands = AttrVal( $FW_wname, "allowedCommands", 0 ) if ( defined($FW_wname) ); if ( $FWallowedCommands && $FWallowedCommands =~ m/\bset-user\b/ ) { $adminMode = 0; return "Forbidden command: set " . $a[1] if ( lc( $a[1] ) eq "create" ); } # states my $states = ( defined( $attr{$name}{rg_states} ) ? $attr{$name}{rg_states} : ( defined( $attr{$name}{rg_showAllStates} ) && $attr{$name}{rg_showAllStates} == 1 ? "home,gotosleep,asleep,awoken,absent,none" : "home,gotosleep,absent,none" ) ); $states = $state . "," . $states if ( $states !~ /$state/ ); $states =~ s/ /,/g; # moods my $moods = ( defined( $attr{$name}{rg_moods} ) ? $attr{$name}{rg_moods} . ",toggle" : "calm,relaxed,happy,excited,lonely,sad,bored,stressed,uncomfortable,sleepy,angry,toggle" ); $moods = $mood . "," . $moods if ( $moods !~ /$mood/ ); $moods =~ s/ /,/g; # locations my $locations = ( defined( $attr{$name}{rg_locations} ) ? $attr{$name}{rg_locations} : "" ); if ( $locations !~ /$location/ && $locations ne "" ) { $locations = ":" . $location . "," . $locations; } elsif ( $locations ne "" ) { $locations = ":" . $locations; } $locations =~ s/ /,/g; my $usage = "Unknown argument " . $a[1] . ", choose one of state:$states"; $usage .= " mood:$moods"; $usage .= " location$locations"; if ($adminMode) { $usage .= " create:wakeuptimer"; $usage .= ",locationMap" if ( ReadingsVal( $name, "locationLat", "-" ) ne "-" && ReadingsVal( $name, "locationLong", "-" ) ne "-" ); } # silentSet if ( $a[1] eq "silentSet" ) { $silent = 1; my $first = shift @a; $a[0] = $first; } # states if ( $a[1] eq "state" || $a[1] eq "home" || $a[1] eq "gotosleep" || $a[1] eq "asleep" || $a[1] eq "awoken" || $a[1] eq "absent" || $a[1] eq "none" || $a[1] eq "gone" ) { my $newstate; # if not direct if ( $a[1] eq "state" && defined( $a[2] ) && ( $a[2] eq "home" || $a[2] eq "gotosleep" || $a[2] eq "asleep" || $a[2] eq "awoken" || $a[2] eq "absent" || $a[2] eq "none" || $a[2] eq "gone" ) ) { $newstate = $a[2]; } elsif ( defined( $a[2] ) ) { return "Invalid 2nd argument, choose one of home gotosleep asleep awoken absent none "; } else { $newstate = $a[1]; } $newstate = "none" if ( $newstate eq "gone" ); Log3 $name, 2, "GUEST set $name " . $newstate if ( !$silent ); # if state changed if ( $state ne $newstate ) { readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "lastState", $state ); readingsBulkUpdate( $hash, "state", $newstate ); my $datetime = TimeNow(); # reset mood my $mood_default = ( defined( $attr{$name}{"rg_moodDefault"} ) ) ? $attr{$name}{"rg_moodDefault"} : "calm"; my $mood_sleepy = ( defined( $attr{$name}{"rg_moodSleepy"} ) ) ? $attr{$name}{"rg_moodSleepy"} : "sleepy"; if ( $mood ne "-" && ( $newstate eq "gone" || $newstate eq "none" || $newstate eq "absent" || $newstate eq "asleep" ) ) { Log3 $name, 4, "GUEST $name: implicit mood change caused by state " . $newstate; GUEST_Set( $hash, $name, "silentSet", "mood", "-" ); } elsif ( $mood ne $mood_sleepy && ( $newstate eq "gotosleep" || $newstate eq "awoken" ) ) { Log3 $name, 4, "GUEST $name: implicit mood change caused by state " . $newstate; GUEST_Set( $hash, $name, "silentSet", "mood", $mood_sleepy ); } elsif ( ( $mood eq "-" || $mood eq $mood_sleepy ) && $newstate eq "home" ) { Log3 $name, 4, "GUEST $name: implicit mood change caused by state " . $newstate; GUEST_Set( $hash, $name, "silentSet", "mood", $mood_default ); } # if state is asleep, start sleep timer readingsBulkUpdate( $hash, "lastSleep", $datetime ) if ( $newstate eq "asleep" ); # if prior state was asleep, update sleep statistics if ( $state eq "asleep" && ReadingsVal( $name, "lastSleep", "" ) ne "" ) { readingsBulkUpdate( $hash, "lastAwake", $datetime ); readingsBulkUpdate( $hash, "lastDurSleep", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastSleep", "" ) ) ); readingsBulkUpdate( $hash, "lastDurSleep_cr", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastSleep", "" ), "min" ) ); } # calculate presence state my $newpresence = ( $newstate ne "none" && $newstate ne "gone" && $newstate ne "absent" ) ? "present" : "absent"; # stop any running wakeup-timers in case state changed my $wakeupState = ReadingsVal( $name, "wakeup", 0 ); if ($wakeupState > 0) { my $wakeupDeviceList = AttrVal( $name, "rg_wakeupDevice", 0 ); for my $wakeupDevice ( split /,/, $wakeupDeviceList ) { next if !$wakeupDevice; if ( defined( $defs{$wakeupDevice} ) && $defs{$wakeupDevice}{TYPE} eq "dummy" ) { # forced-stop only if resident is not present anymore if ( $newpresence eq "present" ) { Log3 $name, 4, "ROOMMATE $name: ending wakeup-timer $wakeupDevice"; fhem "set $wakeupDevice:FILTER=running!=0 end"; } else { Log3 $name, 4, "ROOMMATE $name: stopping wakeup-timer $wakeupDevice"; fhem "set $wakeupDevice:FILTER=running!=0 stop"; } } } } # if presence changed if ( $newpresence ne $presence ) { readingsBulkUpdate( $hash, "presence", $newpresence ); # update location my @location_home = split( ' ', AttrVal( $name, "rg_locationHome", "home" ) ); my @location_underway = split( ' ', AttrVal( $name, "rg_locationUnderway", "underway" ) ); my @location_wayhome = split( ' ', AttrVal( $name, "rg_locationWayhome", "wayhome" ) ); my $searchstring = quotemeta($location); if ( $newpresence eq "present" ) { if ( !grep( m/^$searchstring$/, @location_home ) && $location ne $location_home[0] ) { Log3 $name, 4, "GUEST $name: implicit location change caused by state " . $newstate; GUEST_Set( $hash, $name, "silentSet", "location", $location_home[0] ); } } else { if ( !$silent && !grep( m/^$searchstring$/, @location_underway ) && $location ne $location_underway[0] ) { Log3 $name, 4, "GUEST $name: implicit location change caused by state " . $newstate; GUEST_Set( $hash, $name, "silentSet", "location", $location_underway[0] ); } } # reset wayhome if ( ReadingsVal( $name, "wayhome", 1 ) > 0 ) { readingsBulkUpdate( $hash, "wayhome", "0" ); } # update statistics if ( $newpresence eq "present" ) { readingsBulkUpdate( $hash, "lastArrival", $datetime ); # absence duration if ( ReadingsVal( $name, "lastDeparture", "-" ) ne "-" ) { readingsBulkUpdate( $hash, "lastDurAbsence", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastDeparture", "-" ) ) ); readingsBulkUpdate( $hash, "lastDurAbsence_cr", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastDeparture", "-" ), "min" ) ); } } else { readingsBulkUpdate( $hash, "lastDeparture", $datetime ); # presence duration if ( ReadingsVal( $name, "lastArrival", "-" ) ne "-" ) { readingsBulkUpdate( $hash, "lastDurPresence", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastArrival", "-" ) ) ); readingsBulkUpdate( $hash, "lastDurPresence_cr", RESIDENTStk_TimeDiff( $datetime, ReadingsVal( $name, "lastArrival", "-" ), "min" ) ); } } # adjust linked objects if ( defined( $attr{$name}{"rg_passPresenceTo"} ) && $attr{$name}{"rg_passPresenceTo"} ne "" ) { my @linkedObjects = split( ' ', $attr{$name}{"rg_passPresenceTo"} ); foreach my $object (@linkedObjects) { if ( defined( $defs{$object} ) && $defs{$object} ne $name && defined( $defs{$object}{TYPE} ) && ( $defs{$object}{TYPE} eq "ROOMMATE" || $defs{$object}{TYPE} eq "GUEST" ) && ReadingsVal( $object, "state", "" ) ne "gone" && ReadingsVal( $object, "state", "" ) ne "none" ) { fhem("set $object $newstate"); } } } } # clear readings if guest is gone if ( $newstate eq "none" ) { readingsBulkUpdate( $hash, "lastArrival", "-" ) if ( ReadingsVal( $name, "lastArrival", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastAwake", "-" ) if ( ReadingsVal( $name, "lastAwake", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastDurAbsence", "-" ) if ( ReadingsVal( $name, "lastDurAbsence", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastDurSleep", "-" ) if ( ReadingsVal( $name, "lastDurSleep", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastLocation", "-" ) if ( ReadingsVal( $name, "lastLocation", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastSleep", "-" ) if ( ReadingsVal( $name, "lastSleep", "-" ) ne "-" ); readingsBulkUpdate( $hash, "lastMood", "-" ) if ( ReadingsVal( $name, "lastMood", "-" ) ne "-" ); readingsBulkUpdate( $hash, "location", "-" ) if ( ReadingsVal( $name, "location", "-" ) ne "-" ); readingsBulkUpdate( $hash, "mood", "-" ) if ( ReadingsVal( $name, "mood", "-" ) ne "-" ); } # calculate duration timers GUEST_DurationTimer( $hash, $silent ); readingsEndUpdate( $hash, 1 ); # enable or disable AutoGone timer if ( $newstate eq "absent" ) { GUEST_AutoGone($hash); } elsif ( $state eq "absent" ) { RESIDENTStk_RemoveInternalTimer( "AutoGone", $hash ); } } } # mood elsif ( $a[1] eq "mood" ) { if ( defined( $a[2] ) && $a[2] ne "" ) { Log3 $name, 2, "GUEST set $name mood " . $a[2] if ( !$silent ); readingsBeginUpdate($hash) if ( !$silent ); if ( $a[2] eq "toggle" && ReadingsVal( $name, "lastMood", "" ) ne "" ) { readingsBulkUpdate( $hash, "mood", ReadingsVal( $name, "lastMood", "" ) ); readingsBulkUpdate( $hash, "lastMood", $mood ); } elsif ( $mood ne $a[2] ) { readingsBulkUpdate( $hash, "lastMood", $mood ) if ( $mood ne "-" ); readingsBulkUpdate( $hash, "mood", $a[2] ); } readingsEndUpdate( $hash, 1 ) if ( !$silent ); } else { return "Invalid 2nd argument, choose one of mood toggle"; } } # location elsif ( $a[1] eq "location" ) { if ( defined( $a[2] ) && $a[2] ne "" ) { Log3 $name, 2, "GUEST set $name location " . $a[2] if ( !$silent ); if ( $location ne $a[2] ) { my $searchstring; readingsBeginUpdate($hash) if ( !$silent ); # read attributes my @location_home = split( ' ', AttrVal( $name, "rg_locationHome", "home" ) ); my @location_underway = split( ' ', AttrVal( $name, "rg_locationUnderway", "underway" ) ); my @location_wayhome = split( ' ', AttrVal( $name, "rg_locationWayhome", "wayhome" ) ); $searchstring = quotemeta($location); readingsBulkUpdate( $hash, "lastLocation", $location ) if ( $location ne "wayhome" && !grep( m/^$searchstring$/, @location_underway ) ); readingsBulkUpdate( $hash, "location", $a[2] ) if ( $a[2] ne "wayhome" ); # wayhome detection $searchstring = quotemeta($location); if ( ( $a[2] eq "wayhome" || grep( m/^$searchstring$/, @location_wayhome ) ) && ( $presence eq "absent" ) ) { Log3 $name, 3, "GUEST $name: on way back home from $location"; readingsBulkUpdate( $hash, "wayhome", "1" ) if ( ReadingsVal( $name, "wayhome", "0" ) ne "1" ); } readingsEndUpdate( $hash, 1 ) if ( !$silent ); # auto-updates $searchstring = quotemeta( $a[2] ); if ( ( $a[2] eq "home" || grep( m/^$searchstring$/, @location_home ) ) && $state ne "home" && $state ne "gotosleep" && $state ne "asleep" && $state ne "awoken" && $state ne "initialized" ) { Log3 $name, 4, "GUEST $name: implicit state change caused by location " . $a[2]; GUEST_Set( $hash, $name, "silentSet", "state", "home" ); } elsif ( ( $a[2] eq "underway" || grep( m/^$searchstring$/, @location_underway ) ) && $state ne "gone" && $state ne "none" && $state ne "absent" && $state ne "initialized" ) { Log3 $name, 4, "GUEST $name: implicit state change caused by location " . $a[2]; GUEST_Set( $hash, $name, "silentSet", "state", "absent" ); } } } else { return "Invalid 2nd argument, choose one of location "; } } # create elsif ( lc( $a[1] ) eq "create" ) { if ( !defined( $a[2] ) ) { return "Invalid 2nd argument, choose one of wakeuptimer locationMap "; } elsif ( lc( $a[2] ) eq "wakeuptimer" ) { my $i = "1"; my $wakeuptimerName = $name . "_wakeuptimer" . $i; my $created = 0; until ($created) { if ( defined( $defs{$wakeuptimerName} ) ) { $i++; $wakeuptimerName = $name . "_wakeuptimer" . $i; } else { my $sortby = AttrVal( $name, "sortby", -1 ); $sortby++; # create new dummy device fhem "define $wakeuptimerName dummy"; fhem "attr $wakeuptimerName alias Wake-up Timer $i"; fhem "attr $wakeuptimerName comment Auto-created by GUEST module for use with RESIDENTS Toolkit"; fhem "attr $wakeuptimerName devStateIcon OFF:general_aus\@red:reset running:general_an\@green:stop .*:general_an\@orange:nextRun%20OFF"; fhem "attr $wakeuptimerName group " . $attr{$name}{group} if ( defined( $attr{$name}{group} ) ); fhem "attr $wakeuptimerName icon time_timer"; fhem "attr $wakeuptimerName room " . $attr{$name}{room} if ( defined( $attr{$name}{room} ) ); fhem "attr $wakeuptimerName setList nextRun:OFF,00:00,00:15,00:30,00:45,01:00,01:15,01:30,01:45,02:00,02:15,02:30,02:45,03:00,03:15,03:30,03:45,04:00,04:15,04:30,04:45,05:00,05:15,05:30,05:45,06:00,06:15,06:30,06:45,07:00,07:15,07:30,07:45,08:00,08:15,08:30,08:45,09:00,09:15,09:30,09:45,10:00,10:15,10:30,10:45,11:00,11:15,11:30,11:45,12:00,12:15,12:30,12:45,13:00,13:15,13:30,13:45,14:00,14:15,14:30,14:45,15:00,15:15,15:30,15:45,16:00,16:15,16:30,16:45,17:00,17:15,17:30,17:45,18:00,18:15,18:30,18:45,19:00,19:15,19:30,19:45,20:00,20:15,20:30,20:45,21:00,21:15,21:30,21:45,22:00,22:15,22:30,22:45,23:00,23:15,23:30,23:45 reset:noArg trigger:noArg start:noArg stop:noArg end:noArg"; fhem "attr $wakeuptimerName userattr wakeupUserdevice"; fhem "attr $wakeuptimerName sortby " . $sortby if ($sortby); fhem "attr $wakeuptimerName wakeupUserdevice $name"; fhem "attr $wakeuptimerName webCmd nextRun"; # register slave device my $wakeupDevice = AttrVal( $name, "rg_wakeupDevice", 0 ); if ( !$wakeupDevice ) { fhem "attr $name rg_wakeupDevice $wakeuptimerName"; } elsif ( $wakeupDevice !~ /(.*,?)($wakeuptimerName)(.*,?)/ ) { fhem "attr $name rg_wakeupDevice " . $wakeupDevice . ",$wakeuptimerName"; } # trigger first update fhem "set $wakeuptimerName nextRun OFF"; $created = 1; } } return "Dummy $wakeuptimerName and other pending devices created and pre-configured.\nYou may edit Macro_$wakeuptimerName to define your wake-up actions\nand at_$wakeuptimerName for optional at-device adjustments."; } elsif ( lc( $a[2] ) eq "locationmap" ) { my $locationmapName = $name . "_map"; if ( defined( $defs{$locationmapName} ) ) { return "Device $locationmapName existing already, delete it first to have it re-created."; } else { my $sortby = AttrVal( $name, "sortby", -1 ); $sortby++; # create new weblink device fhem "define $locationmapName weblink htmlCode { '' }"; fhem "attr $locationmapName alias Current Location"; fhem "attr $locationmapName comment Auto-created by GUEST module"; fhem "attr $locationmapName group " . $attr{$name}{group} if ( defined( $attr{$name}{group} ) ); fhem "attr $locationmapName room " . $attr{$name}{room} if ( defined( $attr{$name}{room} ) ); } return "Weblink device $locationmapName was created."; } } # return usage hint else { return $usage; } return undef; } ############################################################################################################ # # Begin of helper functions # ############################################################################################################ ################################### sub GUEST_AutoGone($;$) { my ( $mHash, @a ) = @_; my $hash = ( $mHash->{HASH} ) ? $mHash->{HASH} : $mHash; my $name = $hash->{NAME}; RESIDENTStk_RemoveInternalTimer( "AutoGone", $hash ); if ( ReadingsVal( $name, "state", "home" ) eq "absent" ) { my ( $date, $time, $y, $m, $d, $hour, $min, $sec, $timestamp, $timeDiff ); my $timestampNow = gettimeofday(); my $timeout = ( defined( $attr{$name}{rg_autoGoneAfter} ) ? $attr{$name}{rg_autoGoneAfter} : "16" ); ( $date, $time ) = split( ' ', $hash->{READINGS}{state}{TIME} ); ( $y, $m, $d ) = split( '-', $date ); ( $hour, $min, $sec ) = split( ':', $time ); $m -= 01; $timestamp = timelocal( $sec, $min, $hour, $d, $m, $y ); $timeDiff = $timestampNow - $timestamp; if ( $timeDiff >= $timeout * 3600 ) { Log3 $name, 3, "GUEST $name: AutoGone timer changed state to 'gone'"; GUEST_Set( $hash, $name, "silentSet", "state", "gone" ); } else { my $runtime = $timestamp + $timeout * 3600; Log3 $name, 4, "GUEST $name: AutoGone timer scheduled: $runtime"; RESIDENTStk_InternalTimer( "AutoGone", $runtime, "GUEST_AutoGone", $hash, 1 ); } } return undef; } ################################### sub GUEST_DurationTimer($;$) { my ( $mHash, @a ) = @_; my $hash = ( $mHash->{HASH} ) ? $mHash->{HASH} : $mHash; my $name = $hash->{NAME}; my $state = ReadingsVal( $name, "state", "initialized" ); my $silent = ( defined( $a[0] ) && $a[0] eq "1" ) ? 1 : 0; my $timestampNow = gettimeofday(); my $diff; my $durPresence = "0"; my $durAbsence = "0"; my $durSleep = "0"; RESIDENTStk_RemoveInternalTimer( "DurationTimer", $hash ); if ( !defined( $attr{$name}{rg_noDuration} ) || $attr{$name}{rg_noDuration} == 0 ) { # presence timer if ( ReadingsVal( $name, "presence", "absent" ) eq "present" && ReadingsVal( $name, "lastArrival", "-" ) ne "-" ) { $durPresence = $timestampNow - time_str2num( ReadingsVal( $name, "lastArrival", "" ) ); } # absence timer if ( ReadingsVal( $name, "presence", "present" ) eq "absent" && ReadingsVal( $name, "lastDeparture", "-" ) ne "-" ) { $durAbsence = $timestampNow - time_str2num( ReadingsVal( $name, "lastDeparture", "" ) ); } # sleep timer if ( ReadingsVal( $name, "state", "home" ) eq "asleep" && ReadingsVal( $name, "lastSleep", "-" ) ne "-" ) { $durSleep = $timestampNow - time_str2num( ReadingsVal( $name, "lastSleep", "" ) ); } my $durPresence_hr = ( $durPresence > 0 ) ? RESIDENTStk_sec2time($durPresence) : "00:00:00"; my $durPresence_cr = ( $durPresence > 60 ) ? int( $durPresence / 60 + 0.5 ) : 0; my $durAbsence_hr = ( $durAbsence > 0 ) ? RESIDENTStk_sec2time($durAbsence) : "00:00:00"; my $durAbsence_cr = ( $durAbsence > 60 ) ? int( $durAbsence / 60 + 0.5 ) : 0; my $durSleep_hr = ( $durSleep > 0 ) ? RESIDENTStk_sec2time($durSleep) : "00:00:00"; my $durSleep_cr = ( $durSleep > 60 ) ? int( $durSleep / 60 + 0.5 ) : 0; readingsBeginUpdate($hash) if ( !$silent ); readingsBulkUpdate( $hash, "durTimerPresence_cr", $durPresence_cr ) if ( ReadingsVal( $name, "durTimerPresence_cr", "" ) ne $durPresence_cr ); readingsBulkUpdate( $hash, "durTimerPresence", $durPresence_hr ) if ( ReadingsVal( $name, "durTimerPresence", "" ) ne $durPresence_hr ); readingsBulkUpdate( $hash, "durTimerAbsence_cr", $durAbsence_cr ) if ( ReadingsVal( $name, "durTimerAbsence_cr", "" ) ne $durAbsence_cr ); readingsBulkUpdate( $hash, "durTimerAbsence", $durAbsence_hr ) if ( ReadingsVal( $name, "durTimerAbsence", "" ) ne $durAbsence_hr ); readingsBulkUpdate( $hash, "durTimerSleep_cr", $durSleep_cr ) if ( ReadingsVal( $name, "durTimerSleep_cr", "" ) ne $durSleep_cr ); readingsBulkUpdate( $hash, "durTimerSleep", $durSleep_hr ) if ( ReadingsVal( $name, "durTimerSleep", "" ) ne $durSleep_hr ); readingsEndUpdate( $hash, 1 ) if ( !$silent ); } RESIDENTStk_InternalTimer( "DurationTimer", $timestampNow + 60, "GUEST_DurationTimer", $hash, 1 ) if ( $state ne "none" ); return undef; } ################################### sub GUEST_SetLocation($$$;$$$$$$) { my ( $name, $location, $trigger, $id, $time, $lat, $long, $address, $device ) = @_; my $hash = $defs{$name}; my $state = ReadingsVal( $name, "state", "initialized" ); my $presence = ReadingsVal( $name, "presence", "present" ); my $currLocation = ReadingsVal( $name, "location", "-" ); my $currWayhome = ReadingsVal( $name, "wayhome", "0" ); my $currLat = ReadingsVal( $name, "locationLat", "-" ); my $currLong = ReadingsVal( $name, "locationLong", "-" ); my $currAddr = ReadingsVal( $name, "locationAddr", "" ); $id = "-" if ( !$id || $id eq "" ); $lat = "-" if ( !$lat || $lat eq "" ); $long = "-" if ( !$long || $long eq "" ); $address = "" if ( !$address ); $time = "" if ( !$time ); $device = "" if ( !$device ); Log3 $name, 5, "GUEST $name: received location information: id=$id name=$location trig=$trigger date=$time lat=$lat long=$long address:$address device=$device"; my $searchstring; readingsBeginUpdate($hash); # read attributes my @location_home = split( ' ', AttrVal( $name, "rg_locationHome", "home" ) ); my @location_underway = split( ' ', AttrVal( $name, "rg_locationUnderway", "underway" ) ); my @location_wayhome = split( ' ', AttrVal( $name, "rg_locationWayhome", "wayhome" ) ); $searchstring = quotemeta($location); # update locationPresence readingsBulkUpdate( $hash, "locationPresence", "present" ) if ( $trigger == 1 ); readingsBulkUpdate( $hash, "locationPresence", "absent" ) if ( $trigger == 0 ); # check for implicit state change # my $stateChange = 0; # home if ( $location eq "home" || grep( m/^$searchstring$/, @location_home ) ) { Log3 $name, 5, "GUEST $name: received signal from home location"; # home if ( $state ne "home" && $state ne "gotosleep" && $state ne "asleep" && $state ne "awoken" && $trigger eq "1" ) { $stateChange = 1; } # absent elsif ($state ne "gone" && $state ne "none" && $state ne "absent" && $trigger eq "0" ) { $stateChange = 2; } } # underway elsif ($location eq "underway" || $location eq "wayhome" || grep( m/^$searchstring$/, @location_underway ) || grep( m/^$searchstring$/, @location_wayhome ) ) { Log3 $name, 5, "GUEST $name: received signal from underway location"; # absent $stateChange = 2 if ( $state ne "gone" && $state ne "none" && $state ne "absent" ); } # wayhome if ( $location eq "wayhome" || ( grep( m/^$searchstring$/, @location_wayhome ) && $trigger eq "0" ) ) { Log3 $name, 5, "GUEST $name: wayhome signal received"; # wayhome=true if ( ( ( $location eq "wayhome" && $trigger eq "1" ) || ( $location ne "wayhome" && $trigger eq "0" ) ) && ReadingsVal( $name, "wayhome", "0" ) ne "1" ) { Log3 $name, 3, "GUEST $name: on way back home from $location"; readingsBulkUpdate( $hash, "wayhome", "1" ); } # wayhome=false elsif ($location eq "wayhome" && $trigger eq "0" && ReadingsVal( $name, "wayhome", "0" ) ne "0" ) { Log3 $name, 3, "GUEST $name: seems not to be on way back home anymore"; readingsBulkUpdate( $hash, "wayhome", "0" ); } } # activate wayhome tracing when reaching another location while wayhome=1 elsif ( $stateChange == 0 && $trigger == 1 && $currWayhome == 1 ) { Log3 $name, 3, "GUEST $name: seems to stay at $location before coming home"; readingsBulkUpdate( $hash, "wayhome", "2" ); } # revert wayhome during active wayhome tracing elsif ( $stateChange == 0 && $trigger == 0 && $currWayhome == 2 ) { Log3 $name, 3, "GUEST $name: finally on way back home from $location"; readingsBulkUpdate( $hash, "wayhome", "1" ); } my $currLongDiff = 0; my $currLatDiff = 0; $currLongDiff = maxNum( ReadingsVal( $name, "lastLocationLong", 0 ), $currLong ) - minNum( ReadingsVal( $name, "lastLocationLong", 0 ), $currLong ) if ( $currLong ne "-" ); $currLatDiff = maxNum( ReadingsVal( $name, "lastLocationLat", 0 ), $currLat ) - minNum( ReadingsVal( $name, "lastLocationLat", 0 ), $currLat ) if ( $currLat ne "-" ); if ( $trigger == 1 && ( $stateChange > 0 || ReadingsVal( $name, "lastLocation", "-" ) ne $currLocation || $currLongDiff > 0.00002 || $currLatDiff > 0.00002 ) ) { Log3 $name, 5, "GUEST $name: archiving last known location"; readingsBulkUpdate( $hash, "lastLocationLat", $currLat ); readingsBulkUpdate( $hash, "lastLocationLong", $currLong ); readingsBulkUpdate( $hash, "lastLocationAddr", $currAddr ) if ( $currAddr ne "" ); readingsBulkUpdate( $hash, "lastLocation", $currLocation ); } readingsBulkUpdate( $hash, "locationLat", $lat ); readingsBulkUpdate( $hash, "locationLong", $long ); if ( $address ne "" ) { readingsBulkUpdate( $hash, "locationAddr", $address ); } elsif ( $currAddr ne "" ) { readingsBulkUpdate( $hash, "locationAddr", "-" ); } readingsBulkUpdate( $hash, "location", $location ); readingsEndUpdate( $hash, 1 ); # trigger state change if ( $stateChange > 0 ) { Log3 $name, 4, "GUEST $name: implicit state change caused by location " . $location; GUEST_Set( $hash, $name, "silentSet", "state", "home" ) if $stateChange == 1; GUEST_Set( $hash, $name, "silentSet", "state", "absent" ) if $stateChange == 2; } } ################################### sub GUEST_StartInternalTimers($$) { my ($hash) = @_; GUEST_AutoGone($hash); GUEST_DurationTimer($hash); } 1; =pod =item helper =begin html

GUEST

=end html =begin html_DE

GUEST

=end html_DE =cut