diff --git a/fhem/CHANGED b/fhem/CHANGED index ef5cffb8d..0fef5c6ec 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,6 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. - SVN + - feature: new modules 10_RESIDENTS, 20_ROOMMATE and 20_GUEST added (loredo) - feature: LUXTRONIK2: attribute 'doStatistics' calculates boiler gradients - feature: GEOFANCY: support both apps, Geofency.app and Geofancy.app - feature: LightScene: added attribute lightSceneRestoreOnlyIfChanged diff --git a/fhem/FHEM/10_RESIDENTS.pm b/fhem/FHEM/10_RESIDENTS.pm new file mode 100644 index 000000000..c7633dd96 --- /dev/null +++ b/fhem/FHEM/10_RESIDENTS.pm @@ -0,0 +1,991 @@ +# $Id$ +############################################################################## +# +# 10_RESIDENTS.pm +# An FHEM Perl module to ease resident administration. +# +# 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 . +# +# +# Version: 1.0.0 +# +# Major Version History: +# - 1.0.0 - 2014-02-08 +# -- First release +# +############################################################################## + +package main; + +use strict; +use warnings; +use Time::Local; +use Data::Dumper; + +sub RESIDENTS_Set($@); +sub RESIDENTS_Define($$); +sub RESIDENTS_Notify($$); +sub RESIDENTS_Undefine($$); + +################################### +sub RESIDENTS_Initialize($) { + my ($hash) = @_; + + Log3 $hash, 5, "RESIDENTS_Initialize: Entering"; + + $hash->{SetFn} = "RESIDENTS_Set"; + $hash->{DefFn} = "RESIDENTS_Define"; + $hash->{NotifyFn} = "RESIDENTS_Notify"; + $hash->{UndefFn} = "RESIDENTS_Undefine"; + $hash->{AttrList} = + "rgr_showAllStates:0,1 rgr_states " . $readingFnAttributes; +} + +################################### +sub RESIDENTS_Define($$) { + my ( $hash, $def ) = @_; + my $name = $hash->{NAME}; + my $name_attr; + + Log3 $name, 5, "RESIDENTS $name: called function RESIDENTS_Define()"; + + $hash->{TYPE} = "RESIDENTS"; + + # attr alias + $name_attr = "alias"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = "Residents"; + } + + # attr devStateIcon + $name_attr = "devStateIcon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = +'.*home:status_available:absent .*absent:status_away_1:home .*gone:status_standby:home .*none:control_building_empty .*gotosleep:status_night:asleep .*asleep:status_night:awoken .*awoken:status_available:home'; + } + + # attr group + $name_attr = "group"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = "Home State"; + } + + # attr icon + $name_attr = "icon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = "control_building_filled"; + } + + # attr room + $name_attr = "room"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = "Residents"; + } + + # attr webCmd + $name_attr = "webCmd"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + $attr{$name}{$name_attr} = "state"; + } + + return undef; +} + +################################### +sub RESIDENTS_Undefine($$) { + my ( $hash, $name ) = @_; + + # delete child roommates + if ( defined( $hash->{ROOMMATES} ) + && $hash->{ROOMMATES} ne "" ) + { + my @registeredRoommates = + split( /,/, $hash->{ROOMMATES} ); + + foreach my $child (@registeredRoommates) { + fhem( "delete " . $child ); + Log3 $name, 3, "RESIDENTS $name: deleted device $child"; + } + } + + # delete child guests + if ( defined( $hash->{GUESTS} ) + && $hash->{GUESTS} ne "" ) + { + my @registeredGuests = + split( /,/, $hash->{GUESTS} ); + + foreach my $child (@registeredGuests) { + fhem( "delete " . $child ); + Log3 $name, 3, "RESIDENTS $name: deleted device $child"; + } + } + + return undef; +} + +################################### +sub RESIDENTS_Notify($$) { + my ( $hash, $dev ) = @_; + my $devName = $dev->{NAME}; + my $hashName = $hash->{NAME}; + my $hashName_attr; + + # process child notifies + if ( $devName ne $hashName ) { + my @registeredRoommates = + split( /,/, $hash->{ROOMMATES} ) + if ( defined( $hash->{ROOMMATES} ) + && $hash->{ROOMMATES} ne "" ); + + my @registeredGuests = + split( /,/, $hash->{GUESTS} ) + if ( defined( $hash->{GUESTS} ) + && $hash->{GUESTS} ne "" ); + + # process only registered ROOMMATE or GUEST devices + if ( ( @registeredRoommates && $devName ~~ @registeredRoommates ) + || ( @registeredGuests && $devName ~~ @registeredGuests ) ) + { + + return + if ( !$dev->{CHANGED} ); # Some previous notify deleted the array. + + foreach my $change ( @{ $dev->{CHANGED} } ) { + + # state changed + if ( $change !~ /:/ || $change =~ /wayhome:/ ) { + Log3 $hash, 4, + "RESIDENTS " + . $hashName . ": " + . $devName + . ": notify about change to $change"; + + RESIDENTS_UpdateReadings($hash); + } + + # activity + if ( $change !~ /:/ ) { + + # get user realname + my $realnamesrc = ( + defined( $attr{$devName}{rr_realname} ) + && $attr{$devName}{rr_realname} ne "" + ? $attr{$devName}{rr_realname} + : "group" + ); + my $realname = ( + defined( $attr{$devName}{$realnamesrc} ) + && $attr{$devName}{$realnamesrc} ne "" + ? $attr{$devName}{$realnamesrc} + : $devName + ); + + # update statistics + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "lastActivity", $change ); + readingsBulkUpdate( $hash, "lastActivityBy", $realname ); + readingsEndUpdate( $hash, 1 ); + } + } + } + } + + return; +} + +################################### +sub RESIDENTS_Set($@) { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $state = + ( defined( $hash->{READINGS}{state}{VAL} ) ) + ? $hash->{READINGS}{state}{VAL} + : "initialized"; + my $roommates = ( $hash->{ROOMMATES} ? $hash->{ROOMMATES} : "" ); + my $guests = ( $hash->{GUESTS} ? $hash->{GUESTS} : "" ); + + Log3 $name, 5, "RESIDENTS $name: called function RESIDENTS_Set()"; + + return "No Argument given" if ( !defined( $a[1] ) ); + + # states + my $states = ( + defined( $attr{$name}{rgr_states} ) ? $attr{$name}{rgr_states} + : ( + defined( $attr{$name}{rgr_showAllStates} ) + && $attr{$name}{rgr_showAllStates} == 1 + ? "home,gotosleep,asleep,awoken,absent,gone" + : "home,gotosleep,absent,gone" + ) + ); + $states = $state . "," . $states + if ( $state ne "initialized" && $states !~ /$state/ ); + + my $usage = + "Unknown argument " . $a[1] . ", choose one of addRoommate addGuest"; + $usage .= " state:$states"; + $usage .= " removeRoommate:" . $roommates if ( $roommates ne "" ); + $usage .= " removeGuest:" . $guests if ( $guests ne "" ); + + # 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 "gone" ) + { + my $newstate; + my $presence = "absent"; + + # 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 "gone" ) + ) + { + $newstate = $a[2]; + } + elsif ( defined( $a[2] ) ) { + return +"Invalid 2nd argument, choose one of home gotosleep asleep awoken absent gone "; + } + else { + $newstate = $a[1]; + } + + Log3 $name, 2, "RESIDENTS set $name " . $newstate; + + # loop through every roommate + if ( defined( $hash->{ROOMMATES} ) + && $hash->{ROOMMATES} ne "" ) + { + my @registeredRoommates = + split( /,/, $hash->{ROOMMATES} ); + + foreach my $roommate (@registeredRoommates) { + if ( defined( $defs{$roommate} ) + && $defs{$roommate}{READINGS}{state} ne $newstate ) + { + fhem "set $roommate silentSet state $newstate"; + } + } + } + + # loop through every guest + if ( defined( $hash->{GUESTS} ) + && $hash->{GUESTS} ne "" ) + { + $newstate = "none" if ( $newstate eq "gone" ); + + my @registeredGuests = + split( /,/, $hash->{GUESTS} ); + + foreach my $guest (@registeredGuests) { + if ( defined( $defs{$guest} ) + && $defs{$guest}{READINGS}{state}{VAL} ne "none" + && $defs{$guest}{READINGS}{state}{VAL} ne $newstate ) + { + fhem "set $guest silentSet state $newstate"; + } + } + } + } + + # addRoommate + elsif ( $a[1] eq "addRoommate" ) { + Log3 $name, 2, "RESIDENTS set $name " . $a[1] . " " . $a[2] + if ( defined( $a[2] ) ); + + my $rr_name; + my $rr_name_attr; + + if ( $a[2] ne "" ) { + $rr_name = "rr_" . $a[2]; + + # define roommate + if ( !defined( $defs{$rr_name} ) ) { + fhem( "define " . $rr_name . " ROOMMATE " . $name ); + if ( defined( $defs{$rr_name} ) ) { + fhem "set $rr_name silentSet state home"; + Log3 $name, 3, + "RESIDENTS $name: created new device $rr_name"; + } + } + else { + return "Can't create, device $rr_name already existing."; + } + + } + else { + return "No Argument given, choose one of name "; + } + } + + # removeRoommate + elsif ( $a[1] eq "removeRoommate" ) { + Log3 $name, 2, "RESIDENTS set $name " . $a[1] . " " . $a[2] + if ( defined( $a[2] ) ); + + if ( $a[2] ne "" ) { + my $rr_name = $a[2]; + + # delete roommate + if ( defined( $defs{$rr_name} ) ) { + Log3 $name, 3, "RESIDENTS $name: deleted device $rr_name" + if fhem( "delete " . $rr_name ); + } + } + else { + return "No Argument given, choose one of name "; + } + } + + # addGuest + elsif ( $a[1] eq "addGuest" ) { + Log3 $name, 2, "RESIDENTS set $name " . $a[1] . " " . $a[2] + if ( defined( $a[2] ) ); + + my $rg_name; + my $rg_name_attr; + + if ( $a[2] ne "" ) { + $rg_name = "rg_" . $a[2]; + + # define guest + if ( !defined( $defs{$rg_name} ) ) { + fhem( "define " . $rg_name . " GUEST " . $name ); + if ( defined( $defs{$rg_name} ) ) { + fhem "set $rg_name silentSet state none"; + Log3 $name, 3, + "RESIDENTS $name: created new device $rg_name"; + } + } + else { + return "Can't create, device $rg_name already existing."; + } + + } + else { + return "No Argument given, choose one of name "; + } + } + + # removeGuest + elsif ( $a[1] eq "removeGuest" ) { + Log3 $name, 2, "RESIDENTS set $name " . $a[1] . " " . $a[2] + if ( defined( $a[2] ) ); + + if ( $a[2] ne "" ) { + my $rg_name = $a[2]; + + # delete guest + if ( defined( $defs{$rg_name} ) ) { + Log3 $name, 3, "RESIDENTS $name: deleted device $rg_name" + if fhem( "delete " . $rg_name ); + } + } + else { + return "No Argument given, choose one of name "; + } + } + + # register + elsif ( $a[1] eq "register" ) { + if ( defined( $a[2] ) && $a[2] ne "" ) { + return "No such device " . $a[2] + if ( !defined( $defs{ $a[2] } ) ); + + # ROOMMATE + if ( $defs{ $a[2] }{TYPE} eq "ROOMMATE" ) { + Log3 $name, 4, "RESIDENTS $name: " . $a[2] . " registered"; + + # update readings + $roommates .= ( $roommates eq "" ? $a[2] : "," . $a[2] ) + if ( $roommates !~ /$a[2]/ ); + + $hash->{ROOMMATES} = $roommates; + } + + # GUEST + elsif ( $defs{ $a[2] }{TYPE} eq "GUEST" ) { + Log3 $name, 4, "RESIDENTS $name: " . $a[2] . " registered"; + + # update readings + $guests .= ( $guests eq "" ? $a[2] : "," . $a[2] ) + if ( $guests !~ /$a[2]/ ); + + $hash->{GUESTS} = $guests; + } + + # unsupported + else { + return "Device type is not supported."; + } + + } + else { + return "No Argument given, choose one of ROOMMATE GUEST "; + } + } + + # unregister + elsif ( $a[1] eq "unregister" ) { + if ( defined( $a[2] ) && $a[2] ne "" ) { + return "No such device " . $a[2] + if ( !defined( $defs{ $a[2] } ) ); + + # ROOMMATE + if ( $defs{ $a[2] }{TYPE} eq "ROOMMATE" ) { + Log3 $name, 4, "RESIDENTS $name: " . $a[2] . " unregistered"; + + # update readings + my $replace = "," . $a[2]; + $roommates =~ s/$replace//g; + $replace = $a[2] . ","; + $roommates =~ s/^$replace//g; + $roommates =~ s/^$a[2]//g; + + $hash->{ROOMMATES} = $roommates; + } + + # GUEST + elsif ( $defs{ $a[2] }{TYPE} eq "GUEST" ) { + Log3 $name, 4, "RESIDENTS $name: " . $a[2] . " unregistered"; + + # update readings + my $replace = "," . $a[2]; + $guests =~ s/$replace//g; + $replace = $a[2] . ","; + $guests =~ s/^$replace//g; + $guests =~ s/^$a[2]//g; + + $hash->{GUESTS} = $guests; + } + + # unsupported + else { + return "Device type is not supported."; + } + + } + else { + return "No Argument given, choose one of ROOMMATE GUEST "; + } + + RESIDENTS_UpdateReadings($hash); + } + + # return usage hint + else { + return $usage; + } + + return undef; +} + +############################################################################################################ +# +# Begin of helper functions +# +############################################################################################################ + +sub RESIDENTS_UpdateReadings (@) { + my ($hash) = @_; + my $state = + ( defined $hash->{READINGS}{state}{VAL} ) ? $hash->{READINGS}{state}{VAL} + : ( defined $hash->{STATE} ) ? $hash->{STATE} + : "undefined"; + my $name = $hash->{NAME}; + + 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 $state_total = 0; + my $state_totalPresent = 0; + my $state_totalAbsent = 0; + my $state_totalGuests = 0; + my $state_guestDev = 0; + my $wayhome = 0; + my $newstate; + my $presence = "absent"; + + my @registeredRoommates = + split( /,/, $hash->{ROOMMATES} ) + if ( defined( $hash->{ROOMMATES} ) + && $hash->{ROOMMATES} ne "" ); + + my @registeredGuests = + split( /,/, $hash->{GUESTS} ) + if ( defined( $hash->{GUESTS} ) + && $hash->{GUESTS} ne "" ); + + # count child states for ROOMMATE devices + foreach my $roommate (@registeredRoommates) { + $state_total++; + + if ( defined( $defs{$roommate}{READINGS}{state}{VAL} ) ) { + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "home" ) { + $state_home++; + $state_totalPresent++; + } + + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "gotosleep" ) { + $state_gotosleep++; + $state_totalPresent++; + } + + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "asleep" ) { + $state_asleep++; + $state_totalPresent++; + } + + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "awoken" ) { + $state_awoken++; + $state_totalPresent++; + } + + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "absent" ) { + $state_absent++; + $state_totalAbsent++; + } + + if ( $defs{$roommate}{READINGS}{state}{VAL} eq "gone" ) { + $state_gone++; + $state_totalAbsent++; + } + } + + if ( defined( $defs{$roommate}{READINGS}{wayhome}{VAL} ) ) { + $wayhome += $defs{$roommate}{READINGS}{wayhome}{VAL}; + } + } + + # count child states for GUEST devices + foreach my $guest (@registeredGuests) { + $state_guestDev++; + + if ( defined( $defs{$guest}{READINGS}{state}{VAL} ) ) { + if ( $defs{$guest}{READINGS}{state}{VAL} eq "home" ) { + $state_home++; + $state_totalPresent++; + $state_totalGuests++; + $state_total++; + } + + if ( $defs{$guest}{READINGS}{state}{VAL} eq "gotosleep" ) { + $state_gotosleep++; + $state_totalPresent++; + $state_totalGuests++; + $state_total++; + } + + if ( $defs{$guest}{READINGS}{state}{VAL} eq "asleep" ) { + $state_asleep++; + $state_totalPresent++; + $state_totalGuests++; + $state_total++; + } + + if ( $defs{$guest}{READINGS}{state}{VAL} eq "awoken" ) { + $state_awoken++; + $state_totalPresent++; + $state_totalGuests++; + $state_total++; + } + + if ( $defs{$guest}{READINGS}{state}{VAL} eq "absent" ) { + $state_absent++; + $state_totalAbsent++; + $state_totalGuests++; + $state_total++; + } + } + + if ( defined( $defs{$guest}{READINGS}{wayhome}{VAL} ) ) { + $wayhome += $defs{$guest}{READINGS}{wayhome}{VAL}; + } + } + + # update counter + readingsBeginUpdate($hash); + + readingsBulkUpdate( $hash, "residentsTotal", $state_total ) + if ( !defined( $hash->{READINGS}{residentsTotal}{VAL} ) + || $hash->{READINGS}{residentsTotal}{VAL} ne $state_total ); + + readingsBulkUpdate( $hash, "residentsGuests", $state_totalGuests ) + if ( !defined( $hash->{READINGS}{residentsGuests}{VAL} ) + || $hash->{READINGS}{residentsGuests}{VAL} ne $state_totalGuests ); + + readingsBulkUpdate( $hash, "residentsTotalPresent", $state_totalPresent ) + if ( !defined( $hash->{READINGS}{residentsTotalPresent}{VAL} ) + || $hash->{READINGS}{residentsTotalPresent}{VAL} ne + $state_totalPresent ); + + readingsBulkUpdate( $hash, "residentsTotalAbsent", $state_totalAbsent ) + if ( !defined( $hash->{READINGS}{residentsTotalAbsent}{VAL} ) + || $hash->{READINGS}{residentsTotalAbsent}{VAL} ne $state_totalAbsent ); + + readingsBulkUpdate( $hash, "residentsHome", $state_home ) + if ( !defined( $hash->{READINGS}{residentsHome}{VAL} ) + || $hash->{READINGS}{residentsHome}{VAL} ne $state_home ); + + readingsBulkUpdate( $hash, "residentsGotosleep", $state_gotosleep ) + if ( !defined( $hash->{READINGS}{residentsGotosleep}{VAL} ) + || $hash->{READINGS}{residentsGotosleep}{VAL} ne $state_gotosleep ); + + readingsBulkUpdate( $hash, "residentsAsleep", $state_asleep ) + if ( !defined( $hash->{READINGS}{residentsAsleep}{VAL} ) + || $hash->{READINGS}{residentsAsleep}{VAL} ne $state_asleep ); + + readingsBulkUpdate( $hash, "residentsAwoken", $state_awoken ) + if ( !defined( $hash->{READINGS}{residentsAwoken}{VAL} ) + || $hash->{READINGS}{residentsAwoken}{VAL} ne $state_awoken ); + + readingsBulkUpdate( $hash, "residentsAbsent", $state_absent ) + if ( !defined( $hash->{READINGS}{residentsAbsent}{VAL} ) + || $hash->{READINGS}{residentsAbsent}{VAL} ne $state_absent ); + + readingsBulkUpdate( $hash, "residentsGone", $state_gone ) + if ( !defined( $hash->{READINGS}{residentsGone}{VAL} ) + || $hash->{READINGS}{residentsGone}{VAL} ne $state_gone ); + + readingsBulkUpdate( $hash, "residentsTotalWayhome", $wayhome ) + if ( !defined( $hash->{READINGS}{residentsTotalWayhome}{VAL} ) + || $hash->{READINGS}{residentsTotalWayhome}{VAL} ne $wayhome ); + + # + # state calculation + # + + # gotosleep + if ( $state_home == 0 + && $state_gotosleep > 0 + && $state_asleep >= 0 + && $state_awoken == 0 ) + { + $newstate = "gotosleep"; + } + + # asleep + elsif ($state_home == 0 + && $state_gotosleep == 0 + && $state_asleep > 0 + && $state_awoken == 0 ) + { + $newstate = "asleep"; + } + + # awoken + elsif ($state_home == 0 + && $state_gotosleep >= 0 + && $state_asleep >= 0 + && $state_awoken > 0 ) + { + $newstate = "awoken"; + } + + # general presence + elsif ($state_home > 0 + || $state_gotosleep > 0 + || $state_asleep > 0 + || $state_awoken > 0 ) + { + $newstate = "home"; + } + + # absent + elsif ($state_absent > 0 + && $state_home == 0 + && $state_gotosleep == 0 + && $state_asleep == 0 + && $state_awoken == 0 ) + { + $newstate = "absent"; + } + + # gone + elsif ($state_gone > 0 + && $state_absent == 0 + && $state_home == 0 + && $state_gotosleep == 0 + && $state_asleep == 0 + && $state_awoken == 0 ) + { + $newstate = "gone"; + } + + # none + elsif ($state_totalGuests == 0 + && $state_gone == 0 + && $state_absent == 0 + && $state_home == 0 + && $state_gotosleep == 0 + && $state_asleep == 0 + && $state_awoken == 0 ) + { + $newstate = "none"; + } + + # unspecified; this should not happen + else { + $newstate = "unspecified"; + } + + # calculate presence state + $presence = "present" + if ( $newstate ne "gone" + && $newstate ne "none" + && $newstate ne "absent" ); + + Log3 $name, 4, +"RESIDENTS $name: calculation result - residentsTotal:$state_total residentsGuests:$state_totalGuests residentsTotalPresent:$state_totalPresent residentsTotalAbsent:$state_totalAbsent residentsHome:$state_home residentsGotosleep:$state_gotosleep residentsAsleep:$state_asleep residentsAwoken:$state_awoken residentsAbsent:$state_absent residentsGone:$state_gone presence:$presence state:$newstate"; + + # safe current time + my $datetime = FmtDateTime(time); + + # if state changed + if ( !defined( $hash->{READINGS}{state}{VAL} ) + || $state ne $newstate ) + { + # if newstate is asleep, start sleep timer + readingsBulkUpdate( $hash, "lastSleep", $datetime ) + if ( $newstate eq "asleep" ); + + # if prior state was asleep, update sleep statistics + if ( defined( $hash->{READINGS}{state}{VAL} ) + && $state eq "asleep" ) + { + readingsBulkUpdate( $hash, "lastAwake", $datetime ); + readingsBulkUpdate( + $hash, + "lastDurSleep", + RESIDENTS_TimeDiff( + $datetime, $hash->{READINGS}{lastSleep}{VAL} + ) + ); + } + + readingsBulkUpdate( $hash, "lastState", $hash->{READINGS}{state}{VAL} ); + readingsBulkUpdate( $hash, "state", $newstate ); + } + + # if presence changed + if ( !defined( $hash->{READINGS}{presence}{VAL} ) + || $hash->{READINGS}{presence}{VAL} ne $presence ) + { + readingsBulkUpdate( $hash, "presence", $presence ); + + # update statistics + if ( $presence eq "present" ) { + readingsBulkUpdate( $hash, "lastArrival", $datetime ); + + # absence duration + if ( defined( $hash->{READINGS}{lastDeparture}{VAL} ) + && $hash->{READINGS}{lastDeparture}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurAbsence", + RESIDENTS_TimeDiff( + $datetime, $hash->{READINGS}{lastDeparture}{VAL} + ) + ); + } + } + else { + readingsBulkUpdate( $hash, "lastDeparture", $datetime ); + + # presence duration + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) + && $hash->{READINGS}{lastArrival}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurPresence", + RESIDENTS_TimeDiff( + $datetime, $hash->{READINGS}{lastArrival}{VAL} + ) + ); + } + } + + } +} + +sub RESIDENTS_TimeDiff($$) { + my ( $datetimeNow, $datetimeOld ) = @_; + + my ( + $date, $time, $date2, $time2, + $y, $m, $d, $hour, + $min, $sec, $y2, $m2, + $d2, $hour2, $min2, $sec2, + $timestampNow, $timestampOld, $timeDiff, $hours, + $minutes, $seconds + ); + + ( $date, $time ) = split( ' ', $datetimeNow ); + ( $y, $m, $d ) = split( '-', $date ); + ( $hour, $min, $sec ) = split( ':', $time ); + $m -= 01; + $timestampNow = timelocal( $sec, $min, $hour, $d, $m, $y ); + + ( $date2, $time2 ) = split( ' ', $datetimeOld ); + ( $y2, $m2, $d2 ) = split( '-', $date2 ); + ( $hour2, $min2, $sec2 ) = split( ':', $time2 ); + $m2 -= 01; + $timestampOld = timelocal( $sec2, $min2, $hour2, $d2, $m2, $y2 ); + + $timeDiff = $timestampNow - $timestampOld; + $hours = ( $timeDiff < 3600 ? 0 : int( $timeDiff / 3600 ) ); + $timeDiff -= ( $hours == 0 ? 0 : ( $hours * 3600 ) ); + $minutes = ( $timeDiff < 60 ? 0 : int( $timeDiff / 60 ) ); + $seconds = $timeDiff % 60; + + $hours = "0" . $hours if ( $hours < 10 ); + $minutes = "0" . $minutes if ( $minutes < 10 ); + $seconds = "0" . $seconds if ( $seconds < 10 ); + + return "$hours:$minutes:$seconds"; +} + +1; + +=pod +=begin html + + +

RESIDENTS

+ + +=end html + +=begin html_DE +Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. +Die englische Version ist hier zu finden: + + RESIDENTS   + +=end html_DE + +=cut diff --git a/fhem/FHEM/20_GUEST.pm b/fhem/FHEM/20_GUEST.pm new file mode 100644 index 000000000..6448b89ee --- /dev/null +++ b/fhem/FHEM/20_GUEST.pm @@ -0,0 +1,1064 @@ +# $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 . +# +# +# Version: 1.0.0 +# +# Major Version History: +# - 1.0.0 - 2014-02-08 +# -- First release +# +############################################################################## + +package main; + +use strict; +use warnings; +use Time::Local; +use Data::Dumper; + +sub GUEST_Set($@); +sub GUEST_Define($$); +sub GUEST_Undefine($$); + +################################### +sub GUEST_Initialize($) { + my ($hash) = @_; + + Log3 $hash, 5, "GUEST_Initialize: Entering"; + + $hash->{SetFn} = "GUEST_Set"; + $hash->{DefFn} = "GUEST_Define"; + $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 rg_locations rg_moods rg_moodDefault rg_moodSleepy " + . $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; + } + + readingsBeginUpdate($hash); + + # attr alias + $name_attr = "alias"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + my $aliasname = $name; + $aliasname =~ s/^rg_//; + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = $aliasname; + } + + # attr devStateIcon + $name_attr = "devStateIcon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = +".*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"; + } + + # attr group + $name_attr = "group"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "Guests"; + } + + # attr icon + $name_attr = "icon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "scene_visit_guests"; + } + + # attr icon + $name_attr = "rg_realname"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "alias"; + } + + # attr room + $name_attr = "room"; + if ( @registeredResidentgroups + && exists( $attr{ $registeredResidentgroups[0] }{$name_attr} ) + && !exists( $attr{$name}{$name_attr} ) ) + { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = + $attr{ $registeredResidentgroups[0] }{$name_attr}; + } + + # attr sortby + $name_attr = "sortby"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "1"; + } + + # attr webCmd + $name_attr = "webCmd"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "GUEST $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "state:mood"; + } + + # trigger for modified objects + unless ( $modified == 0 ) { + readingsBulkUpdate( $hash, "state", $hash->{READINGS}{state}{VAL} ); + } + + readingsEndUpdate( $hash, 1 ); + + # run AutoGone timer + InternalTimer( gettimeofday() + 10, "ROOMMATE_AutoGone", $hash, 1 ); + + return undef; +} + +################################### +sub GUEST_Undefine($$) { + my ( $hash, $name ) = @_; + + GUEST_RemoveInternalTimer( "AutoGone", $hash ); + GUEST_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"; + } + } + } + + return undef; +} + +################################### +sub GUEST_Set($@) { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $state = + ( defined( $hash->{READINGS}{state}{VAL} ) ) + ? $hash->{READINGS}{state}{VAL} + : "initialized"; + my $presence = + ( defined( $hash->{READINGS}{presence}{VAL} ) ) + ? $hash->{READINGS}{presence}{VAL} + : "undefined"; + my $mood = + ( defined( $hash->{READINGS}{mood}{VAL} ) ) + ? $hash->{READINGS}{mood}{VAL} + : "-"; + my $location = + ( defined( $hash->{READINGS}{location}{VAL} ) ) + ? $hash->{READINGS}{location}{VAL} + : "undefined"; + my $silent = 0; + + Log3 $name, 5, "GUEST $name: called function GUEST_Set()"; + + return "No Argument given" if ( !defined( $a[1] ) ); + + # 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"; + $usage .= " state:$states"; + $usage .= " mood:$moods"; + $usage .= " location$locations"; + +# $usage .= +#" create:wuTimerWd,wuTimerWe,wuTimerMon,wuTimerTue,wuTimerWed,wuTimerThu,wuTimerFri,wuTimerSat,wuTimerSun"; +# $usage .= " compactMode:noArg largeMode:noArg"; + + # 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 gone "; + } + else { + $newstate = $a[1]; + } + + $newstate = "none" if ( $newstate eq "gone" ); + + Log3 $name, 2, "GUEST set $name " . $newstate if ( !$silent ); + + 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 ); + } + + # calculate presence state + my $newpresence = + ( $newstate ne "none" + && $newstate ne "gone" + && $newstate ne "absent" ) + ? "present" + : "absent"; + + # if presence changed + if ( $newpresence ne $presence ) { + readingsBulkUpdate( $hash, "presence", $newpresence ); + + # update location + my @location_home = + ( defined( $attr{$name}{"rg_locationHome"} ) ) + ? split( ' ', $attr{$name}{"rg_locationHome"} ) + : ("home"); + my @location_underway = + ( defined( $attr{$name}{"rg_locationUnderway"} ) ) + ? split( ' ', $attr{$name}{"rg_locationUnderway"} ) + : ("underway"); + 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 ( !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 ( !defined( $hash->{READINGS}{wayhome}{VAL} ) + || $hash->{READINGS}{wayhome}{VAL} ne "0" ) + { + readingsBulkUpdate( $hash, "wayhome", "0" ); + } + + # update statistics + if ( $newpresence eq "present" ) { + readingsBulkUpdate( $hash, "lastArrival", $datetime ); + + # absence duration + if ( defined( $hash->{READINGS}{lastDeparture}{VAL} ) + && $hash->{READINGS}{lastDeparture}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurAbsence", + GUEST_TimeDiff( + $datetime, $hash->{READINGS}{lastDeparture}{VAL} + ) + ); + } + } + else { + readingsBulkUpdate( $hash, "lastDeparture", $datetime ); + + # presence duration + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) + && $hash->{READINGS}{lastArrival}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurPresence", + GUEST_TimeDiff( + $datetime, $hash->{READINGS}{lastArrival}{VAL} + ) + ); + } + } + + # 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" ) + && defined( $defs{$object}{READINGS}{state}{VAL} ) + && $defs{$object}{READINGS}{state}{VAL} ne "gone" + && $defs{$object}{READINGS}{state}{VAL} ne "none" + ) + { + fhem("set $object $newstate"); + } + } + } + } + + # clear readings if guest is gone + if ( $newstate eq "none" ) { + readingsBulkUpdate( $hash, "lastArrival", "-" ) + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) ); + readingsBulkUpdate( $hash, "lastAwake", "-" ) + if ( defined( $hash->{READINGS}{lastAwake}{VAL} ) ); + readingsBulkUpdate( $hash, "lastDurAbsence", "-" ) + if ( defined( $hash->{READINGS}{lastDurAbsence}{VAL} ) ); + readingsBulkUpdate( $hash, "lastDurSleep", "-" ) + if ( defined( $hash->{READINGS}{lastDurSleep}{VAL} ) ); + readingsBulkUpdate( $hash, "lastLocation", "-" ) + if ( defined( $hash->{READINGS}{lastLocation}{VAL} ) ); + readingsBulkUpdate( $hash, "lastSleep", "-" ) + if ( defined( $hash->{READINGS}{lastSleep}{VAL} ) ); + readingsBulkUpdate( $hash, "lastMood", "-" ) + if ( defined( $hash->{READINGS}{lastMood}{VAL} ) ); + readingsBulkUpdate( $hash, "location", "-" ) + if ( defined( $hash->{READINGS}{location}{VAL} ) ); + readingsBulkUpdate( $hash, "mood", "-" ) + if ( defined( $hash->{READINGS}{mood}{VAL} ) ); + } + + # 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" ) { + GUEST_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" ) { + if ( defined( $hash->{READINGS}{lastMood}{VAL} ) ) { + readingsBulkUpdate( $hash, "mood", + $hash->{READINGS}{lastMood}{VAL} ); + 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 = + ( defined( $attr{$name}{"rg_locationHome"} ) ) + ? split( ' ', $attr{$name}{"rg_locationHome"} ) + : ("home"); + + my @location_underway = + ( defined( $attr{$name}{"rg_locationUnderway"} ) ) + ? split( ' ', $attr{$name}{"rg_locationUnderway"} ) + : ("underway"); + + my @location_wayhome = + ( defined( $attr{$name}{"rg_locationWayhome"} ) ) + ? split( ' ', $attr{$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 ( !defined( $hash->{READINGS}{wayhome}{VAL} ) + || $hash->{READINGS}{wayhome}{VAL} 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 "; + } + } + + # 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}; + + GUEST_RemoveInternalTimer( "AutoGone", $hash ); + + if ( defined( $hash->{READINGS}{state}{VAL} ) + && $hash->{READINGS}{state}{VAL} 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"; + GUEST_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 = ( $hash->{STATE} ) ? $hash->{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"; + + GUEST_RemoveInternalTimer( "DurationTimer", $hash ); + + # presence timer + if ( defined( $hash->{READINGS}{presence}{VAL} ) + && $hash->{READINGS}{presence}{VAL} eq "present" ) + { + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) + && $hash->{READINGS}{lastArrival}{VAL} ne "-" ) + { + $diff = + $timestampNow - + GUEST_Datetime2Timestamp( $hash->{READINGS}{lastArrival}{VAL} ); + $durPresence = ( $diff / 60 ) % 60; + } + } + + # absence timer + if ( defined( $hash->{READINGS}{presence}{VAL} ) + && $hash->{READINGS}{presence}{VAL} eq "absent" + && defined( $hash->{READINGS}{state}{VAL} ) + && $hash->{READINGS}{state}{VAL} eq "absent" ) + { + if ( defined( $hash->{READINGS}{lastDeparture}{VAL} ) + && $hash->{READINGS}{lastDeparture}{VAL} ne "-" ) + { + $diff = + $timestampNow - + GUEST_Datetime2Timestamp( $hash->{READINGS}{lastDeparture}{VAL} ); + $durAbsence = ( $diff / 60 ) % 60; + } + } + + # sleep timer + if ( defined( $hash->{READINGS}{state}{VAL} ) + && $hash->{READINGS}{state}{VAL} eq "asleep" ) + { + if ( defined( $hash->{READINGS}{lastSleep}{VAL} ) + && $hash->{READINGS}{lastSleep}{VAL} ne "-" ) + { + $diff = + $timestampNow - + GUEST_Datetime2Timestamp( $hash->{READINGS}{lastSleep}{VAL} ); + $durSleep = ( $diff / 60 ) % 60; + } + } + + readingsBeginUpdate($hash) if ( !$silent ); + readingsBulkUpdate( $hash, "durTimerPresence", $durPresence ) + if ( !defined( $hash->{READINGS}{durTimerPresence}{VAL} ) + || $hash->{READINGS}{durTimerPresence}{VAL} ne $durPresence ); + readingsBulkUpdate( $hash, "durTimerAbsence", $durAbsence ) + if ( !defined( $hash->{READINGS}{durTimerAbsence}{VAL} ) + || $hash->{READINGS}{durTimerAbsence}{VAL} ne $durAbsence ); + readingsBulkUpdate( $hash, "durTimerSleep", $durSleep ) + if ( !defined( $hash->{READINGS}{durTimerSleep}{VAL} ) + || $hash->{READINGS}{durTimerSleep}{VAL} ne $durSleep ); + readingsEndUpdate( $hash, 1 ) if ( !$silent ); + + GUEST_InternalTimer( "DurationTimer", $timestampNow + 60, + "GUEST_DurationTimer", $hash, 1 ) + if ( $state ne "none" ); + + return undef; +} + +################################### +sub GUEST_TimeDiff($$) { + my ( $datetimeNow, $datetimeOld ) = @_; + + my $timestampNow = GUEST_Datetime2Timestamp($datetimeNow); + my $timestampOld = GUEST_Datetime2Timestamp($datetimeOld); + my $timeDiff = $timestampNow - $timestampOld; + my $hours = ( $timeDiff < 3600 ? 0 : int( $timeDiff / 3600 ) ); + $timeDiff -= ( $hours == 0 ? 0 : ( $hours * 3600 ) ); + my $minutes = ( $timeDiff < 60 ? 0 : int( $timeDiff / 60 ) ); + my $seconds = $timeDiff % 60; + + $hours = "0" . $hours if ( $hours < 10 ); + $minutes = "0" . $minutes if ( $minutes < 10 ); + $seconds = "0" . $seconds if ( $seconds < 10 ); + + return "$hours:$minutes:$seconds"; +} + +################################### +sub GUEST_Datetime2Timestamp($) { + my ($datetime) = @_; + + my ( $date, $time, $y, $m, $d, $hour, $min, $sec, $timestamp ); + + ( $date, $time ) = split( ' ', $datetime ); + ( $y, $m, $d ) = split( '-', $date ); + ( $hour, $min, $sec ) = split( ':', $time ); + $m -= 01; + $timestamp = timelocal( $sec, $min, $hour, $d, $m, $y ); + + return $timestamp; +} + +################################### +sub GUEST_InternalTimer($$$$$) { + my ( $modifier, $tim, $callback, $hash, $waitIfInitNotDone ) = @_; + + my $mHash; + if ( $modifier eq "" ) { + $mHash = $hash; + } + else { + my $timerName = $hash->{NAME} . "_" . $modifier; + if ( exists( $hash->{TIMER}{$timerName} ) ) { + $mHash = $hash->{TIMER}{$timerName}; + } + else { + $mHash = { + HASH => $hash, + NAME => $hash->{NAME} . "_" . $modifier, + MODIFIER => $modifier + }; + $hash->{TIMER}{$timerName} = $mHash; + } + } + InternalTimer( $tim, $callback, $mHash, $waitIfInitNotDone ); +} + +################################### +sub GUEST_RemoveInternalTimer($$) { + my ( $modifier, $hash ) = @_; + + my $timerName = $hash->{NAME} . "_" . $modifier; + if ( $modifier eq "" ) { + RemoveInternalTimer($hash); + } + else { + my $mHash = $hash->{TIMER}{$timerName}; + if ( defined($mHash) ) { + delete $hash->{TIMER}{$timerName}; + RemoveInternalTimer($mHash); + } + } +} + +1; + +=pod +=begin html + + +

GUEST

+
    + + + Define +
      + define <rg_GuestName> GUEST [<device name of resident group>] +

      + + Provides a special dummy device to represent a guest of your home.
      + Based on the current state and other readings, you may trigger other actions within FHEM.

      + Used by superior module RESIDENTS but may also be used stand-alone.

      + + Example:
      +
        + # Standalone + define rg_Guest GUEST +

        + # Typical group member + define rg_Guest GUEST rgr_Residents # to be member of resident group rgr_Residents +

        + # Member of multiple groups + define rg_Guest GUEST rgr_Residents,rgr_Guests # to be member of resident group rgr_Residents and rgr_Guests +
      +

    + + Please note the RESIDENTS group device needs to be existing before a GUEST device can become a member of it.
    +
    +
    + + + Set +
      + set <rg_FirstName> <command> [<parameter>] +

      + Currently, the following commands are defined.
      +
        +
      • location   -   sets reading 'location'; see attribute rg_locations to adjust list shown in FHEMWEB
      • +
      • mood   -   sets reading 'mood'; see attribute rg_moods to adjust list shown in FHEMWEB
      • +
      • state   home,gotosleep,asleep,awoken,absent,gone   switch between states; see attribute rg_states to adjust list shown in FHEMWEB
      • +
      +
    + +
      + Possible states and their meaning

      +
        + This module differs 6 states:

        + +
          +
        • home - individual is present at home and awake
        • +
        • gotosleep - individual is on it's way to bed
        • +
        • asleep - individual is currently sleeping
        • +
        • awoken - individual just woke up from sleep
        • +
        • absent - individual is not present at home but will be back shortly
        • +
        • none - guest device is disabled
        • +
        + +
      +
    +
    +
    + +
      + Presence correlation to location

      +
        + Under specific circumstances, changing state will automatically change reading 'location' as well.
        +
        + Whenever presence state changes from 'absent' to 'present', the location is set to 'home'. If attribute rg_locationHome was defined, first location from it will be used as home location.
        +
        + Whenever presence state changes from 'present' to 'absent', the location is set to 'underway'. If attribute rg_locationUnderway was defined, first location from it will be used as underway location. +
      +
    +
    +
    + +
      + Auto Gone

      +
        + Whenever an individual is set to 'absent', a trigger is started to automatically change state to 'gone' after a specific timeframe.
        + Default value is 16 hours.
        +
        + This behaviour can be customized by attribute rg_autoGoneAfter. +
      +
    +
    +
    + +
      + Synchronizing presence with other ROOMMATE or GUEST devices

      +
        + If you always leave or arrive at your house together with other roommates or guests, you may enable a synchronization of your presence state for certain individuals.
        + By setting attribute rg_passPresenceTo, those individuals will follow your presence state changes to 'home', 'absent' or 'gone' as you do them with your own device.
        +
        + Please note that individuals with current state 'none' or 'gone' (in case of roommates) will not be touched. +
      +
    +
    +
    + +
      + Location correlation to state

      +
        + Under specific circumstances, changing location will have an effect on the actual state as well.
        +
        + Whenever location is set to 'home', the state is set to 'home' if prior presence state was 'absent'. If attribute rg_locationHome was defined, all of those locations will trigger state change to 'home' as well.
        +
        + Whenever location is set to 'underway', the state is set to 'absent' if prior presence state was 'present'. If attribute rg_locationUnderway was defined, all of those locations will trigger state change to 'absent' as well. Those locations won't appear in reading 'lastLocation'.
        +
        + Whenever location is set to 'wayhome', the reading 'wayhome' is set to '1' if current presence state is 'absent'. If attribute rg_locationWayhome was defined, LEAVING one of those locations will set reading 'wayhome' to '1' as well. So you actually have implicit and explicit options to trigger wayhome.
        + Arriving at home will reset the value of 'wayhome' to '0'. +
      +
    +
    +
    + + + Attributes
    +
        +
      • rg_autoGoneAfter - hours after which state should be auto-set to 'gone' when current state is 'absent'; defaults to 16 hours
      • +
      • rg_locationHome - locations matching these will be treated as being at home; first entry reflects default value to be used with state correlation; separate entries by space; defaults to "home"
      • +
      • rg_locationUnderway - locations matching these will be treated as being underway; first entry reflects default value to be used with state correlation; separate entries by comma or space; defaults to "underway"
      • +
      • rg_locationWayhome - leaving a location matching these will set reading wayhome to 1; separate entries by space; defaults to "wayhome"
      • +
      • rg_locations - list of locations ot be shown in FHEMWEB; separate entries by comma only and do NOT use spaces
      • +
      • rg_moodDefault - the mood that should be set after arriving at home or changing state from awoken to home
      • +
      • rg_moodSleepy - the mood that should be set if state was changed to gotosleep or awoken
      • +
      • rg_moods - list of moods to be shown in FHEMWEB; separate entries by comma only and do NOT use spaces
      • +
      • rg_passPresenceTo - synchronize presence state with other GUEST or GUEST devices; separte devices by space
      • +
      • rg_realname - whenever GUEST wants to use the realname it uses the value of attribute alias or group; defaults to group
      • +
      • rg_showAllStates - states 'asleep' and 'awoken' are hidden by default to allow simple gotosleep process via devStateIcon; defaults to 0
      • +
      • rg_states - list of states ot be shown in FHEMWEB; separate entries by comma only and do NOT use spaces; unsupported states will lead to errors though
      • +
    +
    +
    + +
    + Generated Readings/Events:
    +
        +
      • durTimerAbsence - timer to show the duration of absence from home in minutes
      • +
      • durTimerPresence - timer to show the duration of presence at home in minutes
      • +
      • durTimerSleep - timer to show the duration of sleep in minutes
      • +
      • lastArrival - timestamp of last arrival at home
      • +
      • lastAwake - timestamp of last sleep cycle end
      • +
      • lastDeparture - timestamp of last departure from home
      • +
      • lastDurAbsence - duration of last absence from home in following format: hours:minutes:seconds
      • +
      • lastDurPresence - duration of last presence at home in following format: hours:minutes:seconds
      • +
      • lastDurSleep - duration of last sleep in following format: hours:minutes:seconds
      • +
      • lastLocation - the prior location
      • +
      • lastMood - the prior mood
      • +
      • lastSleep - timestamp of last sleep cycle begin
      • +
      • lastState - the prior state
      • +
      • location - the current location
      • +
      • presence - reflects the home presence state, depending on value of reading 'state' (can be 'present' or 'absent')
      • +
      • mood - the current mood
      • +
      • state - reflects the current state
      • +
      • wayhome - depending on current location, it can become '1' if individual is on his/her way back home

      • +
        + The following readings will be set to '-' if state was changed to 'none':
        + lastArrival, lastDurAbsence, lastLocation, lastMood, location, mood +
    + +
+ +=end html + +=begin html_DE +Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. +Die englische Version ist hier zu finden: + + GUEST   + +=end html_DE + +=cut diff --git a/fhem/FHEM/20_ROOMMATE.pm b/fhem/FHEM/20_ROOMMATE.pm new file mode 100644 index 000000000..3ae027571 --- /dev/null +++ b/fhem/FHEM/20_ROOMMATE.pm @@ -0,0 +1,1053 @@ +# $Id$ +############################################################################## +# +# 20_ROOMMATE.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 . +# +# +# Version: 1.0.0 +# +# Major Version History: +# - 1.0.0 - 2014-02-08 +# -- First release +# +############################################################################## + +package main; + +use strict; +use warnings; +use Time::Local; +use Data::Dumper; + +sub ROOMMATE_Set($@); +sub ROOMMATE_Define($$); +sub ROOMMATE_Undefine($$); + +################################### +sub ROOMMATE_Initialize($) { + my ($hash) = @_; + + Log3 $hash, 5, "ROOMMATE_Initialize: Entering"; + + $hash->{SetFn} = "ROOMMATE_Set"; + $hash->{DefFn} = "ROOMMATE_Define"; + $hash->{UndefFn} = "ROOMMATE_Undefine"; + $hash->{AttrList} = +"rr_locationHome rr_locationWayhome rr_locationUnderway rr_autoGoneAfter:12,16,24,26,28,30,36,48,60 rr_showAllStates:0,1 rr_realname:group,alias rr_states rr_locations rr_moods rr_moodDefault rr_moodSleepy rr_passPresenceTo " + . $readingFnAttributes; +} + +################################### +sub ROOMMATE_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + my $name = $hash->{NAME}; + my $name_attr; + + Log3 $name, 5, "ROOMMATE $name: called function ROOMMATE_Define()"; + + if ( int(@a) < 2 ) { + my $msg = + "Wrong syntax: define ROOMMATE [RESIDENTS-DEVICE-NAMES]"; + Log3 $name, 4, $msg; + return $msg; + } + + $hash->{TYPE} = "ROOMMATE"; + + 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, + "ROOMMATE $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, +"ROOMMATE $name: Unable to register at RESIDENTS device $parent (not existing)"; + next; + } + + if ( $defs{$parent}{TYPE} ne "RESIDENTS" ) { + Log3 $name, 3, +"ROOMMATE $name: Device $parent is not a RESIDENTS device (wrong type)"; + next; + } + + fhem("set $parent register $name"); + $hash->{RESIDENTGROUPS} .= $parent . ","; + Log3 $name, 4, + "ROOMMATE $name: Registered at RESIDENTS device $parent"; + } + } + else { + $modified = 0; + } + + readingsBeginUpdate($hash); + + # attr alias + $name_attr = "alias"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "Status"; + } + + # attr devStateIcon + $name_attr = "devStateIcon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = +".*home:user_available:absent .*absent:user_away:home .*gone:user_ext_away:home .*gotosleep:scene_toilet:asleep .*asleep:scene_sleeping:awoken .*awoken:scene_sleeping_alternat:home .*:user_unknown"; + } + + # attr group + $name_attr = "group"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + my $groupname = $name; + $groupname =~ s/^rr_//; + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = $groupname; + } + + # attr icon + $name_attr = "icon"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "status_available"; + } + + # attr room + $name_attr = "room"; + if ( @registeredResidentgroups + && exists( $attr{ $registeredResidentgroups[0] }{$name_attr} ) + && !exists( $attr{$name}{$name_attr} ) ) + { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = + $attr{ $registeredResidentgroups[0] }{$name_attr}; + } + + # attr sortby + $name_attr = "sortby"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "0"; + } + + # attr webCmd + $name_attr = "webCmd"; + unless ( exists( $attr{$name}{$name_attr} ) ) { + Log3 $name, 4, "ROOMMATE $name: created new attribute '$name_attr'"; + + $attr{$name}{$name_attr} = "state:mood"; + } + + # trigger for modified objects + unless ( $modified == 0 ) { + readingsBulkUpdate( $hash, "state", $hash->{READINGS}{state}{VAL} ); + } + + readingsEndUpdate( $hash, 1 ); + + # run AutoGone timer + InternalTimer( gettimeofday() + 10, "ROOMMATE_AutoGone", $hash, 1 ); + + return undef; +} + +################################### +sub ROOMMATE_Undefine($$) { + my ( $hash, $name ) = @_; + + ROOMMATE_RemoveInternalTimer( "AutoGone", $hash ); + ROOMMATE_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, + "ROOMMATE $name: Unregistered at RESIDENTS device $parent"; + } + } + } + + return undef; +} + +################################### +sub ROOMMATE_Set($@) { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $state = + ( defined( $hash->{READINGS}{state}{VAL} ) ) + ? $hash->{READINGS}{state}{VAL} + : "initialized"; + my $presence = + ( defined( $hash->{READINGS}{presence}{VAL} ) ) + ? $hash->{READINGS}{presence}{VAL} + : "undefined"; + my $mood = + ( defined( $hash->{READINGS}{mood}{VAL} ) ) + ? $hash->{READINGS}{mood}{VAL} + : "-"; + my $location = + ( defined( $hash->{READINGS}{location}{VAL} ) ) + ? $hash->{READINGS}{location}{VAL} + : "undefined"; + my $silent = 0; + + Log3 $name, 5, "ROOMMATE $name: called function ROOMMATE_Set()"; + + return "No Argument given" if ( !defined( $a[1] ) ); + + # states + my $states = ( + defined( $attr{$name}{rr_states} ) ? $attr{$name}{rr_states} + : ( + defined( $attr{$name}{rr_showAllStates} ) + && $attr{$name}{rr_showAllStates} == 1 + ? "home,gotosleep,asleep,awoken,absent,gone" + : "home,gotosleep,absent,gone" + ) + ); + $states = $state . "," . $states if ( $states !~ /$state/ ); + $states =~ s/ /,/g; + + # moods + my $moods = ( + defined( $attr{$name}{rr_moods} ) + ? $attr{$name}{rr_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}{rr_locations} ) + ? $attr{$name}{rr_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"; + $usage .= " state:$states"; + $usage .= " mood:$moods"; + $usage .= " location$locations"; + +# $usage .= +#" create:wuTimerWd,wuTimerWe,wuTimerMon,wuTimerTue,wuTimerWed,wuTimerThu,wuTimerFri,wuTimerSat,wuTimerSun"; +# $usage .= " compactMode:noArg largeMode:noArg"; + + # 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 "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 "gone" ) + ) + { + $newstate = $a[2]; + } + elsif ( defined( $a[2] ) ) { + return +"Invalid 2nd argument, choose one of home gotosleep asleep awoken absent gone "; + } + else { + $newstate = $a[1]; + } + + Log3 $name, 2, "ROOMMATE set $name " . $newstate if ( !$silent ); + + 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}{"rr_moodDefault"} ) ) + ? $attr{$name}{"rr_moodDefault"} + : "calm"; + my $mood_sleepy = + ( defined( $attr{$name}{"rr_moodSleepy"} ) ) + ? $attr{$name}{"rr_moodSleepy"} + : "sleepy"; + + if ( + $mood ne "-" + && ( $newstate eq "gone" + || $newstate eq "none" + || $newstate eq "absent" + || $newstate eq "asleep" ) + ) + { + Log3 $name, 4, + "ROOMMATE $name: implicit mood change caused by state " + . $newstate; + ROOMMATE_Set( $hash, $name, "silentSet", "mood", "-" ); + } + + elsif ( $mood ne $mood_sleepy + && ( $newstate eq "gotosleep" || $newstate eq "awoken" ) ) + { + Log3 $name, 4, + "ROOMMATE $name: implicit mood change caused by state " + . $newstate; + ROOMMATE_Set( $hash, $name, "silentSet", "mood", $mood_sleepy ); + } + + elsif ( ( $mood eq "-" || $mood eq $mood_sleepy ) + && $newstate eq "home" ) + { + Log3 $name, 4, + "ROOMMATE $name: implicit mood change caused by state " + . $newstate; + ROOMMATE_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" + && defined( $hash->{READINGS}{lastSleep}{VAL} ) ) + { + readingsBulkUpdate( $hash, "lastAwake", $datetime ); + readingsBulkUpdate( + $hash, + "lastDurSleep", + ROOMMATE_TimeDiff( + $datetime, $hash->{READINGS}{lastSleep}{VAL} + ) + ); + } + + # calculate presence state + my $newpresence = + ( $newstate ne "none" + && $newstate ne "gone" + && $newstate ne "absent" ) + ? "present" + : "absent"; + + # if presence changed + if ( $newpresence ne $presence ) { + readingsBulkUpdate( $hash, "presence", $newpresence ); + + # update location + my @location_home = + ( defined( $attr{$name}{"rr_locationHome"} ) ) + ? split( ' ', $attr{$name}{"rr_locationHome"} ) + : ("home"); + my @location_underway = + ( defined( $attr{$name}{"rr_locationUnderway"} ) ) + ? split( ' ', $attr{$name}{"rr_locationUnderway"} ) + : ("underway"); + my $searchstring = quotemeta($location); + + if ( $newpresence eq "present" ) { + if ( !grep( m/^$searchstring$/, @location_home ) + && $location ne $location_home[0] ) + { + Log3 $name, 4, +"ROOMMATE $name: implicit location change caused by state " + . $newstate; + ROOMMATE_Set( $hash, $name, "silentSet", "location", + $location_home[0] ); + } + } + else { + if ( !grep( m/^$searchstring$/, @location_underway ) + && $location ne $location_underway[0] ) + { + Log3 $name, 4, +"ROOMMATE $name: implicit location change caused by state " + . $newstate; + ROOMMATE_Set( $hash, $name, "silentSet", "location", + $location_underway[0] ); + } + } + + # reset wayhome + if ( !defined( $hash->{READINGS}{wayhome}{VAL} ) + || $hash->{READINGS}{wayhome}{VAL} ne "0" ) + { + readingsBulkUpdate( $hash, "wayhome", "0" ); + } + + # update statistics + if ( $newpresence eq "present" ) { + readingsBulkUpdate( $hash, "lastArrival", $datetime ); + + # absence duration + if ( defined( $hash->{READINGS}{lastDeparture}{VAL} ) + && $hash->{READINGS}{lastDeparture}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurAbsence", + ROOMMATE_TimeDiff( + $datetime, $hash->{READINGS}{lastDeparture}{VAL} + ) + ); + } + } + else { + readingsBulkUpdate( $hash, "lastDeparture", $datetime ); + + # presence duration + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) + && $hash->{READINGS}{lastArrival}{VAL} ne "-" ) + { + readingsBulkUpdate( + $hash, + "lastDurPresence", + ROOMMATE_TimeDiff( + $datetime, $hash->{READINGS}{lastArrival}{VAL} + ) + ); + } + } + + # adjust linked objects + if ( defined( $attr{$name}{"rr_passPresenceTo"} ) + && $attr{$name}{"rr_passPresenceTo"} ne "" ) + { + my @linkedObjects = + split( ' ', $attr{$name}{"rr_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" ) + && defined( $defs{$object}{READINGS}{state}{VAL} ) + && $defs{$object}{READINGS}{state}{VAL} ne "gone" + && $defs{$object}{READINGS}{state}{VAL} ne "none" + ) + { + fhem("set $object $newstate"); + } + } + } + } + + # calculate duration timers + ROOMMATE_DurationTimer( $hash, $silent ); + + readingsEndUpdate( $hash, 1 ); + + # enable or disable AutoGone timer + if ( $newstate eq "absent" ) { + ROOMMATE_AutoGone($hash); + } + elsif ( $state eq "absent" ) { + ROOMMATE_RemoveInternalTimer( "AutoGone", $hash ); + } + } + } + + # mood + elsif ( $a[1] eq "mood" ) { + if ( defined( $a[2] ) && $a[2] ne "" ) { + Log3 $name, 2, "ROOMMATE set $name mood " . $a[2] if ( !$silent ); + readingsBeginUpdate($hash) if ( !$silent ); + + if ( $a[2] eq "toggle" ) { + if ( defined( $hash->{READINGS}{lastMood}{VAL} ) ) { + readingsBulkUpdate( $hash, "mood", + $hash->{READINGS}{lastMood}{VAL} ); + 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, "ROOMMATE set $name location " . $a[2] + if ( !$silent ); + + if ( $location ne $a[2] ) { + my $searchstring; + + readingsBeginUpdate($hash) if ( !$silent ); + + # read attributes + my @location_home = + ( defined( $attr{$name}{"rr_locationHome"} ) ) + ? split( ' ', $attr{$name}{"rr_locationHome"} ) + : ("home"); + + my @location_underway = + ( defined( $attr{$name}{"rr_locationUnderway"} ) ) + ? split( ' ', $attr{$name}{"rr_locationUnderway"} ) + : ("underway"); + + my @location_wayhome = + ( defined( $attr{$name}{"rr_locationWayhome"} ) ) + ? split( ' ', $attr{$name}{"rr_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, + "ROOMMATE $name: on way back home from $location"; + readingsBulkUpdate( $hash, "wayhome", "1" ) + if ( !defined( $hash->{READINGS}{wayhome}{VAL} ) + || $hash->{READINGS}{wayhome}{VAL} 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, +"ROOMMATE $name: implicit state change caused by location " + . $a[2]; + ROOMMATE_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, +"ROOMMATE $name: implicit state change caused by location " + . $a[2]; + ROOMMATE_Set( $hash, $name, "silentSet", "state", + "absent" ); + } + } + } + else { + return "Invalid 2nd argument, choose one of location "; + } + } + + # return usage hint + else { + return $usage; + } + + return undef; +} + +############################################################################################################ +# +# Begin of helper functions +# +############################################################################################################ + +################################### +sub ROOMMATE_AutoGone($;$) { + my ( $mHash, @a ) = @_; + my $hash = ( $mHash->{HASH} ) ? $mHash->{HASH} : $mHash; + my $name = $hash->{NAME}; + + ROOMMATE_RemoveInternalTimer( "AutoGone", $hash ); + + if ( defined( $hash->{READINGS}{state}{VAL} ) + && $hash->{READINGS}{state}{VAL} eq "absent" ) + { + my ( $date, $time, $y, $m, $d, $hour, $min, $sec, $timestamp, + $timeDiff ); + my $timestampNow = gettimeofday(); + my $timeout = ( + defined( $attr{$name}{rr_autoGoneAfter} ) + ? $attr{$name}{rr_autoGoneAfter} + : "36" + ); + + ( $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, + "ROOMMATE $name: AutoGone timer changed state to 'gone'"; + ROOMMATE_Set( $hash, $name, "silentSet", "state", "gone" ); + } + else { + my $runtime = $timestamp + $timeout * 3600; + Log3 $name, 4, "ROOMMATE $name: AutoGone timer scheduled: $runtime"; + ROOMMATE_InternalTimer( "AutoGone", $runtime, "ROOMMATE_AutoGone", + $hash, 1 ); + } + } + + return undef; +} + +################################### +sub ROOMMATE_DurationTimer($;$) { + my ( $mHash, @a ) = @_; + my $hash = ( $mHash->{HASH} ) ? $mHash->{HASH} : $mHash; + my $name = $hash->{NAME}; + 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"; + + ROOMMATE_RemoveInternalTimer( "DurationTimer", $hash ); + + # presence timer + if ( defined( $hash->{READINGS}{presence}{VAL} ) + && $hash->{READINGS}{presence}{VAL} eq "present" ) + { + if ( defined( $hash->{READINGS}{lastArrival}{VAL} ) + && $hash->{READINGS}{lastArrival}{VAL} ne "-" ) + { + $diff = + $timestampNow - + ROOMMATE_Datetime2Timestamp( + $hash->{READINGS}{lastArrival}{VAL} ); + $durPresence = ( $diff / 60 ) % 60; + } + } + + # absence timer + if ( defined( $hash->{READINGS}{presence}{VAL} ) + && $hash->{READINGS}{presence}{VAL} eq "absent" ) + { + if ( defined( $hash->{READINGS}{lastDeparture}{VAL} ) + && $hash->{READINGS}{lastDeparture}{VAL} ne "-" ) + { + $diff = + $timestampNow - + ROOMMATE_Datetime2Timestamp( + $hash->{READINGS}{lastDeparture}{VAL} ); + $durAbsence = ( $diff / 60 ) % 60; + } + } + + # sleep timer + if ( defined( $hash->{READINGS}{state}{VAL} ) + && $hash->{READINGS}{state}{VAL} eq "asleep" ) + { + if ( defined( $hash->{READINGS}{lastSleep}{VAL} ) + && $hash->{READINGS}{lastSleep}{VAL} ne "-" ) + { + $diff = + $timestampNow - + ROOMMATE_Datetime2Timestamp( $hash->{READINGS}{lastSleep}{VAL} ); + $durSleep = ( $diff / 60 ) % 60; + } + } + + readingsBeginUpdate($hash) if ( !$silent ); + readingsBulkUpdate( $hash, "durTimerPresence", $durPresence ) + if ( !defined( $hash->{READINGS}{durTimerPresence}{VAL} ) + || $hash->{READINGS}{durTimerPresence}{VAL} ne $durPresence ); + readingsBulkUpdate( $hash, "durTimerAbsence", $durAbsence ) + if ( !defined( $hash->{READINGS}{durTimerAbsence}{VAL} ) + || $hash->{READINGS}{durTimerAbsence}{VAL} ne $durAbsence ); + readingsBulkUpdate( $hash, "durTimerSleep", $durSleep ) + if ( !defined( $hash->{READINGS}{durTimerSleep}{VAL} ) + || $hash->{READINGS}{durTimerSleep}{VAL} ne $durSleep ); + readingsEndUpdate( $hash, 1 ) if ( !$silent ); + + ROOMMATE_InternalTimer( "DurationTimer", $timestampNow + 60, + "ROOMMATE_DurationTimer", $hash, 1 ); + + return undef; +} + +################################### +sub ROOMMATE_TimeDiff($$) { + my ( $datetimeNow, $datetimeOld ) = @_; + + my $timestampNow = ROOMMATE_Datetime2Timestamp($datetimeNow); + my $timestampOld = ROOMMATE_Datetime2Timestamp($datetimeOld); + my $timeDiff = $timestampNow - $timestampOld; + my $hours = ( $timeDiff < 3600 ? 0 : int( $timeDiff / 3600 ) ); + $timeDiff -= ( $hours == 0 ? 0 : ( $hours * 3600 ) ); + my $minutes = ( $timeDiff < 60 ? 0 : int( $timeDiff / 60 ) ); + my $seconds = $timeDiff % 60; + + $hours = "0" . $hours if ( $hours < 10 ); + $minutes = "0" . $minutes if ( $minutes < 10 ); + $seconds = "0" . $seconds if ( $seconds < 10 ); + + return "$hours:$minutes:$seconds"; +} + +################################### +sub ROOMMATE_Datetime2Timestamp($) { + my ($datetime) = @_; + + my ( $date, $time, $y, $m, $d, $hour, $min, $sec, $timestamp ); + + ( $date, $time ) = split( ' ', $datetime ); + ( $y, $m, $d ) = split( '-', $date ); + ( $hour, $min, $sec ) = split( ':', $time ); + $m -= 01; + $timestamp = timelocal( $sec, $min, $hour, $d, $m, $y ); + + return $timestamp; +} + +################################### +sub ROOMMATE_InternalTimer($$$$$) { + my ( $modifier, $tim, $callback, $hash, $waitIfInitNotDone ) = @_; + + my $mHash; + if ( $modifier eq "" ) { + $mHash = $hash; + } + else { + my $timerName = $hash->{NAME} . "_" . $modifier; + if ( exists( $hash->{TIMER}{$timerName} ) ) { + $mHash = $hash->{TIMER}{$timerName}; + } + else { + $mHash = { + HASH => $hash, + NAME => $hash->{NAME} . "_" . $modifier, + MODIFIER => $modifier + }; + $hash->{TIMER}{$timerName} = $mHash; + } + } + InternalTimer( $tim, $callback, $mHash, $waitIfInitNotDone ); +} + +################################### +sub ROOMMATE_RemoveInternalTimer($$) { + my ( $modifier, $hash ) = @_; + + my $timerName = $hash->{NAME} . "_" . $modifier; + if ( $modifier eq "" ) { + RemoveInternalTimer($hash); + } + else { + my $mHash = $hash->{TIMER}{$timerName}; + if ( defined($mHash) ) { + delete $hash->{TIMER}{$timerName}; + RemoveInternalTimer($mHash); + } + } +} + +1; + +=pod +=begin html + + +

ROOMMATE

+
    + + + Define +
      + define <rr_FirstName> ROOMMATE [<device name of resident group>] +

      + + Provides a special dummy device to represent a resident of your home.
      + Based on the current state and other readings, you may trigger other actions within FHEM.

      + Used by superior module RESIDENTS but may also be used stand-alone.

      + + Example:
      +
        + # Standalone + define rr_Manfred ROOMMATE +

        + # Typical group member + define rr_Manfred ROOMMATE rgr_Residents # to be member of resident group rgr_Residents +

        + # Member of multiple groups + define rr_Manfred ROOMMATE rgr_Residents,rgr_Parents # to be member of resident group rgr_Residents and rgr_Parents +

        + # Complex family structure + define rr_Manfred ROOMMATE rgr_Residents,rgr_Parents # Parent + define rr_Lisa ROOMMATE rgr_Residents,rgr_Parents # Parent + define rr_Rick ROOMMATE rgr_Residents,rgr_Children # Child1 + define rr_Alex ROOMMATE rgr_Residents,rgr_Children # Child2 +
      +

    + + Please note the RESIDENTS group device needs to be existing before a ROOMMATE device can become a member of it.
    +
    +
    + + + Set +
      + set <rr_FirstName> <command> [<parameter>] +

      + Currently, the following commands are defined.
      +
        +
      • location   -   sets reading 'location'; see attribute rr_locations to adjust list shown in FHEMWEB
      • +
      • mood   -   sets reading 'mood'; see attribute rr_moods to adjust list shown in FHEMWEB
      • +
      • state   home,gotosleep,asleep,awoken,absent,gone   switch between states; see attribute rr_states to adjust list shown in FHEMWEB
      • +
      +
    + +
      + Possible states and their meaning

      +
        + This module differs 6 states:

        + +
          +
        • home - individual is present at home and awake
        • +
        • gotosleep - individual is on it's way to bed
        • +
        • asleep - individual is currently sleeping
        • +
        • awoken - individual just woke up from sleep
        • +
        • absent - individual is not present at home but will be back shortly
        • +
        • gone - individual is away from home for longer period
        • +
        + +
      +
    +
    +
    + +
      + Presence correlation to location

      +
        + Under specific circumstances, changing state will automatically change reading 'location' as well.
        +
        + Whenever presence state changes from 'absent' to 'present', the location is set to 'home'. If attribute rr_locationHome was defined, first location from it will be used as home location.
        +
        + Whenever presence state changes from 'present' to 'absent', the location is set to 'underway'. If attribute rr_locationUnderway was defined, first location from it will be used as underway location. +
      +
    +
    +
    + +
      + Auto Gone

      +
        + Whenever an individual is set to 'absent', a trigger is started to automatically change state to 'gone' after a specific timeframe.
        + Default value is 36 hours.
        +
        + This behaviour can be customized by attribute rr_autoGoneAfter. +
      +
    +
    +
    + +
      + Synchronizing presence with other ROOMMATE or GUEST devices

      +
        + If you always leave or arrive at your house together with other roommates or guests, you may enable a synchronization of your presence state for certain individuals.
        + By setting attribute rr_passPresenceTo, those individuals will follow your presence state changes to 'home', 'absent' or 'gone' as you do them with your own device.
        +
        + Please note that individuals with current state 'gone' or 'none' (in case of guests) will not be touched. +
      +
    +
    +
    + +
      + Location correlation to state

      +
        + Under specific circumstances, changing location will have an effect on the actual state as well.
        +
        + Whenever location is set to 'home', the state is set to 'home' if prior presence state was 'absent'. If attribute rr_locationHome was defined, all of those locations will trigger state change to 'home' as well.
        +
        + Whenever location is set to 'underway', the state is set to 'absent' if prior presence state was 'present'. If attribute rr_locationUnderway was defined, all of those locations will trigger state change to 'absent' as well. Those locations won't appear in reading 'lastLocation'.
        +
        + Whenever location is set to 'wayhome', the reading 'wayhome' is set to '1' if current presence state is 'absent'. If attribute rr_locationWayhome was defined, LEAVING one of those locations will set reading 'wayhome' to '1' as well. So you actually have implicit and explicit options to trigger wayhome.
        + Arriving at home will reset the value of 'wayhome' to '0'. +
      +
    +
    +
    + + + Attributes
    +
        +
      • rr_autoGoneAfter - hours after which state should be auto-set to 'gone' when current state is 'absent'; defaults to 36 hours
      • +
      • rr_locationHome - locations matching these will be treated as being at home; first entry reflects default value to be used with state correlation; separate entries by space; defaults to "home"
      • +
      • rr_locationUnderway - locations matching these will be treated as being underway; first entry reflects default value to be used with state correlation; separate entries by comma or space; defaults to "underway"
      • +
      • rr_locationWayhome - leaving a location matching these will set reading wayhome to 1; separate entries by space; defaults to "wayhome"
      • +
      • rr_locations - list of locations ot be shown in FHEMWEB; separate entries by comma only and do NOT use spaces
      • +
      • rr_moodDefault - the mood that should be set after arriving at home or changing state from awoken to home
      • +
      • rr_moodSleepy - the mood that should be set if state was changed to gotosleep or awoken
      • +
      • rr_moods - list of moods to be shown in FHEMWEB; separate entries by comma only and do NOT use spaces
      • +
      • rr_passPresenceTo - synchronize presence state with other ROOMMATE or GUEST devices; separte devices by space
      • +
      • rr_realname - whenever ROOMMATE wants to use the realname it uses the value of attribute alias or group; defaults to group
      • +
      • rr_showAllStates - states 'asleep' and 'awoken' are hidden by default to allow simple gotosleep process via devStateIcon; defaults to 0
      • +
      • rr_states - list of states ot be shown in FHEMWEB; separate entries by comma only and do NOT use spaces; unsupported states will lead to errors though
      • +
    +
    +
    + +
    + Generated Readings/Events:
    +
        +
      • durTimerAbsence - timer to show the duration of absence from home in minutes
      • +
      • durTimerPresence - timer to show the duration of presence at home in minutes
      • +
      • durTimerSleep - timer to show the duration of sleep in minutes
      • +
      • lastArrival - timestamp of last arrival at home
      • +
      • lastAwake - timestamp of last sleep cycle end
      • +
      • lastDeparture - timestamp of last departure from home
      • +
      • lastDurAbsence - duration of last absence from home in following format: hours:minutes:seconds
      • +
      • lastDurPresence - duration of last presence at home in following format: hours:minutes:seconds
      • +
      • lastDurSleep - duration of last sleep in following format: hours:minutes:seconds
      • +
      • lastLocation - the prior location
      • +
      • lastMood - the prior mood
      • +
      • lastSleep - timestamp of last sleep cycle begin
      • +
      • lastState - the prior state
      • +
      • location - the current location
      • +
      • presence - reflects the home presence state, depending on value of reading 'state' (can be 'present' or 'absent')
      • +
      • mood - the current mood
      • +
      • state - reflects the current state
      • +
      • wayhome - depending on current location, it can become '1' if individual is on his/her way back home
      • +
    + +
+ +=end html + +=begin html_DE +Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. +Die englische Version ist hier zu finden: + + ROOMMATE   + +=end html_DE + +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index daaa324c5..c6352622d 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -43,6 +43,7 @@ FHEM/10_IT.pm odroegehorn http://forum.fhem.de InterTech FHEM/10_MAX.pm mgehre http://forum.fhem.de MAX FHEM/10_OWServer.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire FHEM/10_ZWave.pm rudolfkoenig http://forum.fhem.de ZWave +FHEM/10_RESIDENTS.pm loredo http://forum.fhem.de Automatisierung FHEM/11_FHT.pm rudolfkoenig http://forum.fhem.de SlowRF FHEM/11_FHT8V.pm rudolfkoenig http://forum.fhem.de SlowRF FHEM/11_OWDevice.pm borisneubert/mfr69bs http://forum.fhem.de 1Wire @@ -66,6 +67,8 @@ FHEM/20_FRM_PWM.pm ntruchsess http://forum.fhem.de Sonstiges FHEM/20_FRM_SERVO.pm ntruchsess http://forum.fhem.de Sonstiges FHEM/20_OWFS.pm mfr69bs http://forum.fhem.de 1Wire (deprecated) FHEM/20_X10.pm borisneubert http://forum.fhem.de SlowRF +FHEM/20_ROOMMATE.pm loredo http://forum.fhem.de Automatisierung +FHEM/20_GUEST.pm loredo http://forum.fhem.de Automatisierung FHEM/21_OWAD.pm pahenning http://forum.fhem.de 1Wire FHEM/21_OWCOUNT.pm pahenning http://forum.fhem.de 1Wire FHEM/21_OWID.pm pahenning http://forum.fhem.de 1Wire