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
+
+
+
+ Define
+
+ define <rgr_ResidentsName> RESIDENTS
+
+
+ Provides a special dummy device to represent a group if individuals living at your home.
+ It locically combines individual states of ROOMMATE and GUEST devices and allows state changes for all members.
+ Based on the current state and other readings, you may trigger other actions within FHEM.
+
+ Example:
+
+ # Standalone
+ define rgr_Residents RESIDENTS
+
+
+
+
+
+
+ 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 7 states:
+
+
+ - home - residents are present at home and awake
+ - gotosleep - all present residents are on it's way to bed
+ - asleep - all present residents are currently sleeping
+ - awoken - all present residents just woke up from sleep
+ - absent - no resident is currently at home but will be back shortly
+ - gone - all residents left home for longer period
+ - none - no active member
+
+
+ Note: State 'none' cannot explicitly be set. Setting state to 'gone' will be handled as 'none' for GUEST member devices.
+
+
+
+
+
+
+
+ Attributes
+
+ - rgr_showAllStates - states 'asleep' and 'awoken' are hidden by default to allow simple gotosleep process via devStateIcon; defaults to 0
+ - rgr_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:
+
+ - 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
+ - lastSleep - timestamp of last sleep cycle begin
+ - lastState - the prior state
+ - presence - reflects the home presence state, depending on value of reading 'state' (can be 'present' or 'absent')
+ - residentsAbsent - number of residents with state 'absent'
+ - residentsAsleep - number of residents with state 'asleep'
+ - residentsAwoken - number of residents with state 'awoken'
+ - residentsGone - number of residents with state 'gone'
+ - residentsGotosleep - number of residents with state 'gotosleep'
+ - residentsGuests - number of active guests who are currently threated as part of the residents scope
+ - residentsHome - number of residents with state 'home'
+ - residentsTotal - total number of all active residents despite their current state
+ - residentsTotalAbsent - number of all residents who are currently underway
+ - residentsTotalPresent - number of all residents who are currently at home
+ - residentsTotalWayhome - number of all active residents who are currently on their way back home
+ - state - reflects the current state
+
+
+
+
+=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