############################################## # $Id: 98_todoist.pm 26953 2023-01-03 12:58:56Z marvin78 $ package main; use strict; use warnings; my $missingModule = ""; eval "use Data::Dumper;1" or $missingModule .= "Data::Dumper "; eval "use JSON;1" or $missingModule .= "JSON "; eval "use Encode;1" or $missingModule .= "Encode "; eval "use Date::Parse;1" or $missingModule .= "Date::Parse "; ####################### # Global variables my $version = "1.3.28"; my $apiUrl = "https://api.todoist.com/sync/v9/"; my $srandUsed; my %gets = ( "version:noArg" => "", ); ## define variables for multi language my %todoist_transtable_EN = ( "check" => "Check", "delete" => "Delete", "refreshList" => "Refresh list", "clearList" => "Delete all elements", "alreadythere" => "is already on the list", "error" => "Error", "clearconfirm" => "Are you sure? This deletes ALL the task in this list permanently.", "delconfirm" => "Are you sure? This deletes the task permanently.", "gotodetail" => "Click to show detail page of todoist device", "newentry" => "Add new task to list", "nolistdata" => "No data for this list", "idnotfound" => "Could not find the task", "today" => "today", "tomorrow" => "tomorrow", "dayaftertomorrow" => "the day after tomorrow", "yesterday" => "yesterday", ); my %todoist_transtable_DE = ( "check" => "Erledigen", "delete" => "Löschen", "refreshList" => "Liste aktualisieren", "clearList" => "Alle Elemente löschen", "alreadythere" => "befindet sich bereits auf der Liste", "error" => "Fehler", "clearconfirm" => "Wirklich alle Elemente löschen?", "delconfirm" => "Task wirklich löschen?", "gotodetail" => "Detailseite des todoist-Devices aufrufen", "newentry" => "Neuen Eintrag zur Liste hinzufügen", "nolistdata" => "List ist leer", "idnotfound" => "Task konnte nicht gefundern werden", "today" => "heute", "tomorrow" => "morgen", "dayaftertomorrow" => "übermorgen", "yesterday" => "gestern", ); my $todoist_tt; sub todoist_Initialize($) { my ($hash) = @_; $hash->{SetFn} = "todoist_Set"; $hash->{GetFn} = "todoist_Get"; $hash->{DefFn} = "todoist_Define"; $hash->{UndefFn} = "todoist_Undefine"; $hash->{AttrFn} = "todoist_Attr"; $hash->{RenameFn} = "todoist_Rename"; $hash->{CopyFn} = "todoist_Copy"; $hash->{DeleteFn} = "todoist_Delete"; $hash->{NotifyFn} = "todoist_Notify"; $hash->{FW_detailFn} = "todoist_detailFn"; $hash->{AttrList} = "disable:1,0 ". "pollInterval ". "do_not_notify ". "getCompleted:1,0 ". "showPriority:1,0 ". "showAssignedBy:1,0 ". "showResponsible:1,0 ". "showParent:1,0 ". "showSection:1,0 ". "showChecked:1,0 ". "showDeleted:1,0 ". "showOrder:1,0 ". "hideId:1,0 ". "autoGetUsers:1,0 ". "avoidDuplicates:1,0 ". "listDivider ". "showDetailWidget:1,0 ". "hideListIfEmpty:1,0 ". "delDeletedLists:1,0 ". "language:EN,DE ". "sslVersion ". $readingFnAttributes; $hash->{NotifyOrderPrefix} = "64-"; ## renew version and language in reload foreach my $d ( sort keys %{ $modules{todoist}{defptr} } ) { my $hash = $modules{todoist}{defptr}{$d}; $hash->{VERSION} = $version; my $lang = AttrVal($hash->{NAME},"language", AttrVal("global","language","EN")); if( $lang eq "DE") { $todoist_tt = \%todoist_transtable_DE; } else{ $todoist_tt = \%todoist_transtable_EN; } } return undef; #return FHEM::Meta::InitMod( __FILE__, $hash ); } sub todoist_Define($$) { my ($hash, $def) = @_; my $now = time(); my $name = $hash->{NAME}; if( !defined($todoist_tt) ){ # in any attribute redefinition readjust language my $lang = AttrVal($name,"language", AttrVal("global","language","EN")); if( $lang eq "DE") { $todoist_tt = \%todoist_transtable_DE; } else{ $todoist_tt = \%todoist_transtable_EN; } } my @a = split( "[ \t][ \t]*", $def ); if ( int(@a) < 2 ) { my $msg = "Wrong syntax: define todoist "; Log3 $name, 4, $msg; return $msg; } return "Cannot define a todoist device. Perl module(s) $missingModule is/are missing." if ( $missingModule ); ## set internal variables $hash->{PID}=$a[2]; $hash->{INTERVAL}=AttrVal($name,"pollInterval",undef)?AttrVal($name,"pollInterval",undef):1800; $hash->{VERSION}=$version; $hash->{MID} = 'todoist_'.$a[2]; # $modules{todoist}{defptr}{ $hash->{MID} } = $hash; #MID for internal purposes ## check if Access Token is needed my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my ($err, $password) = getKeyValue($index); $hash->{helper}{PWD_NEEDED}=1 if ($err || !$password); $hash->{NOTIFYDEV}= "global"; if ($init_done) { ## at first, we delete old readings. List could have changed CommandDeleteReading(undef, "$hash->{NAME} (T|t)ask_.*"); CommandDeleteReading(undef, "$hash->{NAME} listText"); ## set state readingsSingleUpdate($hash,"state","active",1) if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name) ); readingsSingleUpdate($hash,"state","inactive",1) if ($hash->{helper}{PWD_NEEDED} || ReadingsVal($name,"state","-") eq "-"); ## remove timers RemoveInternalTimer($hash,"todoist_GetTasks"); ## start polling todoist_GetTasks($hash) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); } return undef; } # get token from file sub todoist_GetPwd($) { my ($hash) = @_; my $name=$hash->{NAME}; my $pwd=""; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my ($err, $password) = getKeyValue($index); if ($err) { $hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; return undef; } #some decryption if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $pwd.=chr(ord($char)^ord($decode)); $key=$decode.$key; } } return undef if ($pwd eq ""); return $pwd; } ## set error Readings and log sub todoist_ErrorReadings($;$$) { my ($hash,$errorLog,$errorMessage) = @_; my $level=2; if (!defined($errorLog)) { $level=3; $errorLog="no data"; } $errorMessage="no data" if (!defined($errorMessage)); if (defined($hash->{helper}{errorData}) && $hash->{helper}{errorData} ne "") { $errorLog=$hash->{helper}{errorData}; } if (defined($hash->{helper}{errorMessage}) && $hash->{helper}{errorMessage} ne "") { $errorMessage=$hash->{helper}{errorMessage}; } my $name = $hash->{NAME}; readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash,"error",$errorMessage ); readingsBulkUpdate( $hash,"lastError",$errorMessage ); readingsEndUpdate( $hash, 1 ); Log3 $name,$level, "todoist ($name): Error Message: ".$errorMessage if ($errorMessage ne "no data"); Log3 $name,4, "todoist ($name): Api-Error Callback-data: ".$errorLog; $hash->{helper}{errorData}=""; $hash->{helper}{errorMessage}=""; return undef; } # reorderTasks sub todoist_ReorderTasks ($$) { my ($hash,$cmd) = @_; my $name=$hash->{NAME}; Log3 $name,4, "$name: cmd: ".$cmd; my $pwd=""; my $param; my %commands=(); # some random string for UUID my $uuid = todoist_genUUID(); # JSON String start- and endpoint my $commandsStart="[{"; my $commandsEnd="}]"; my $tType; my $argsStart = "{\"items\": ["; my $argsEnd = "]}"; my $args = ""; ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); # get Task - IDs in order my $tids = $cmd; my @taskIds = split(",",$tids); $tType = "item_reorder"; my $i=0; foreach my $taskId (@taskIds) { $i++; $args .= "," if ($i>1); $args .= "{\"id\":".$taskId.",\"child_order\":".$i."}"; } $args = $argsStart.$args.$argsEnd; Log3 $name,5, "todoist ($name): Data sent to todoist API: ".$args; my $dataArr=$commandsStart.'"type":"'.$tType.'","uuid":"'.$uuid.'","args":'.$args.$commandsEnd; Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".$dataArr; my $data= { token => $pwd, commands => $dataArr }; Log3 $name,4, "todoist ($name): JSON sent to todoist API: ".Dumper($data); my $method="POST"; $param = { url => $apiUrl."sync", data => $data, method => $method, wType => "reorder", timeout => 7, header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } return undef; } # update Task sub todoist_UpdateTask($$$) { my ($hash,$cmd, $type) = @_; my($a, $h) = parseParams($cmd); my $name=$hash->{NAME}; Log3 $name,5, "$name: Type: ".Dumper($type); my $param; my $pwd=""; my %commands=(); my $method; my $taskId=0; my $title; my $tid; ## get Task-ID $tid = @$a[0]; ## check if ID is todoist ID (ID:.*) or title (TITLE:.*) my @temp=split(":",$tid); ## use the todoist ID if (@temp && $temp[0] =~ /id/i) { $taskId = int($temp[1]); $title = $hash->{helper}{"TITLE"}{$temp[1]}; } ## use task content elsif (@temp && $temp[0] =~ /title/i) { $title = encode_utf8($temp[1]); my $nTitle = $title; $nTitle =~ s/ /_/g; $taskId = $hash->{helper}{"TITLES"}{$nTitle} if ($hash->{helper}{"TITLES"}); } elsif (defined($h->{"title"}) || defined($h->{"TITLE"}) || defined($h->{"Title"})) { Log3 $name, 5, "todoist ($name): Debug: ".Dumper($h); $title = $h->{"title"} if ($h->{"title"}); $title = $h->{"TITLE"} if ($h->{"TITLE"}); $title = $h->{"Title"} if ($h->{"Title"}); my $nTitle = $title; $nTitle =~ s/ /_/g; Log3 $name, 5, "todoist ($name): Debug: ".$nTitle; $taskId = $hash->{helper}{"TITLES"}{$nTitle} if ($hash->{helper}{"TITLES"}); } ## use Task-Number else { $tid=int($tid); $taskId=$hash->{helper}{"IDS"}{"Task_".$tid}; $title=ReadingsVal($name,"Task_".sprintf('%03d',$tid),"-"); } # error if we did not get a task id if ($taskId == 0) { map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"idnotfound"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); todoist_ErrorReadings($hash,"Task $title could not be found for $type","task $title not found"); return undef; } # some random string for UUID my $uuid = todoist_genUUID(); # JSON String start- and endpoint my $commandsStart="[{"; my $commandsEnd="}]"; my $tType; my %args=(); ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); ## complete a task if ($type eq "complete") { # variables for the commands parameter $tType = "item_complete"; %args = ( id => $taskId, ); Log3 $name,5, "$name: Args: ".Dumper(%args); $method="POST"; } ## close a task elsif ($type eq "close") { # variables for the commands parameter $tType = "item_close"; %args = ( id => $taskId, ); Log3 $name,5, "$name: Args: ".Dumper(%args); $method="POST"; } ## uncomplete a task elsif ($type eq "uncomplete") { # variables for the commands parameter $tType = "item_uncomplete"; %args = ( id => $taskId, ); Log3 $name,5, "$name: Args: ".Dumper(%args); $method="POST"; } ## move a task elsif ($type eq "move") { # we can avoid duplicates in FHEM. There may still come duplicates coming from another app if ($h->{"parent_id"}) { #if (AttrVal($name,"avoidDuplicates",0) == 1 && todoist_inArray(\@{$hash->{helper}{"TITS"}},$title)) { # map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"alreadythere"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); # todoist_ErrorReadings($hash,"duplicate detected","duplicate detected"); # return undef; #} } $tType = "item_move"; %args = ( id => $taskId, ); ## parent_id $args{'parent_id'} = $h->{"parent_id"} if ($h->{"parent_id"}); $args{'parent_id'} = $h->{"parentID"} if ($h->{"parentID"}); $args{'parent_id'} = $h->{"parentId"} if ($h->{"parentId"}); ## project_id $args{'project_id'} = $h->{"project_id"} if ($h->{"project_id"}); $args{'project_id'} = $h->{"projectID"} if ($h->{"projectID"}); $args{'project_id'} = $h->{"projectId"} if ($h->{"projectId"}); ## section_id $args{'section_id'} = $h->{"section_id"} if ($h->{"section_id"}); $args{'section_id'} = $h->{"sectionID"} if ($h->{"sectionID"}); $args{'section_id'} = $h->{"sectionId"} if ($h->{"sectionId"}); if ($args{'parent_id'}) { my $pid=$args{'parent_id'}; } } ## update a task elsif ($type eq "update") { $tType = "item_update"; %args = ( id => $taskId, ); ## change title $args{'content'} = $h->{"title"} if($h->{'title'}); ## change dueDate $args{'due'}{'string'} = $h->{"dueDate"} if($h->{'dueDate'}); $args{'due'}{'string'} = "" if ($h->{'dueDate'} && $h->{'dueDate'} =~ /(null|none|nix|leer|del)/); ## change dueDate (if someone uses due_date in stead of dueDate) $args{'due'}{'string'} = $h->{"due_date"} if ($h->{'due_date'}); $args{'due'}{'string'} = "" if ($h->{'due_date'} && $h->{'due_date'} =~ /(null|none|nix|leer|del)/); ## change priority $args{'priority'} = int($h->{"priority"}) if ($h->{"priority"}); ## Who is responsible for the task $args{'responsible_uid'} = $h->{"responsibleUid"} if ($h->{"responsibleUid"}); $args{'responsible_uid'} = $h->{"responsible"} if ($h->{"responsible"}); ## who assigned the task? $args{'assigned_by_uid'} = $h->{"assignedByUid"} if ($h->{"assignedByUid"}); $args{'assigned_by_uid'} = $h->{"assignedBy"} if ($h->{"assignedByUid"}); ## order of the task $args{'child_order'} = $h->{"order"} if ($h->{"order"}); ## child order of the task $args{'child_order'} = $h->{"child_order"} if ($h->{"child_order"}); ## remove attribute if ($h->{"remove"}) { my @temp; my @rem = split(",",$h->{"remove"}); foreach my $r (@rem) { $args{'due'} = "" if ($r eq "dueDate" || $r eq "due_date"); $args{'responsible_uid'} = "" if ($r eq "responsibleUid" || $r eq "responsible"); $args{'assigned_by_uid'} = 0 if ($r eq "assignedByUid" || $r eq "assignedBy"); if ($r eq "parent_id" || $r eq "parentID" || $r eq "parentId" || $r eq "child_order") { $args{'child_order'} = 1; $args{'parent_id'} = ""; } } ## Debug #Log3 $name, 1, "wunderlist ($name): Debug: ".Dumper($datas{'remove'}); } ## Debug #Log3 $name, 1, "todoist ($name): Debug: ".Dumper(%datas); $method="POST"; } ## delete a task elsif ($type eq "delete") { $tType = "item_delete"; %args = ( id => $taskId, ); $method="POST"; } else { return undef; } Log3 $name,5, "todoist ($name): Data Array sent to todoist API: ".Dumper(%args); my $dataArr=$commandsStart.'"type":"'.$tType.'","temp_id":"'.$taskId.'","uuid":"'.$uuid.'","args":'.encode_json(\%args).$commandsEnd; Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".$dataArr; my $data= { token => $pwd, commands => $dataArr }; Log3 $name,4, "todoist ($name): JSON sent to todoist API: ".Dumper($data); $param = { url => $apiUrl."sync", data => $data, tTitle => $title, method => $method, wType => $type, taskId => $taskId, timeout => 7, header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } return undef; } # create Task sub todoist_CreateTask($$) { my ($hash,$cmd) = @_; my($a, $h) = parseParams($cmd); my $name=$hash->{NAME}; my $param; my $pwd=""; my $assigne_id=""; ## we try to send a due_date (in developement) my @tmp = split( ":", join(" ",@$a) ); my $titleS=$tmp[0]; $titleS = $h->{"title"} if ($h->{"title"}); my $title = encode_utf8($titleS); my $check=1; # we can avoid duplicates in FHEM. There may still come duplicates coming from another app if (AttrVal($name,"avoidDuplicates",0) == 1 && todoist_inArray(\@{$hash->{helper}{"TITS"}},$title)) { $check=-1; } if ($check==1) { ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); if ($pwd) { # JSON String start- and endpoint my $commandsStart="[{"; my $commandsEnd="}]"; # some random string for UUID my $uuid = todoist_genUUID(); # some random string for tempID my $tempId = todoist_genUUID(); Log3 $name,5, "$name: hash: ".Dumper($hash); my %args=(); # data array for API - we could transfer more data %args = ( project_id => $hash->{PID}, content => $titleS, ); ## check for dueDate as Parameter or part of title - push to hash if (!$tmp[1] && $h->{"dueDate"}) { ## parameter $args{'due'}{'string'} = $h->{"dueDate"}; } elsif ($tmp[1]) { ## title $args{'due'}{'string'} = $tmp[1]; } else { } ## if someone uses due_date - no problem $args{'date_string'} = $h->{"due_date"} if ($h->{"due_date"}); $args{'date_string'} = encode_utf8($args{'date_string'}); ## Task parent_id $args{'parent_id'} = int($h->{"parent_id"}) if ($h->{"parent_id"}); $args{'parent_id'} = int($h->{"parentID"}) if ($h->{"parentID"}); $args{'parent_id'} = int($h->{"parentId"}) if ($h->{"parentId"}); my $parentId = 0; $parentId = %args{'parent_id'} if (%args{'parent_id'}); ## Task priority $args{'priority'} = $h->{"priority"} if ($h->{"priority"}); ## who is responsible for the task? $args{'responsible_uid'} = $h->{"responsibleUid"} if ($h->{"responsibleUid"}); $args{'responsible_uid'} = $h->{"responsible"} if ($h->{"responsible"}); ## who assigned the task? $args{'assigned_by_uid'} = $h->{"assignedByUid"} if ($h->{"assignedByUid"}); $args{'assigned_by_uid'} = $h->{"assignedBy"} if ($h->{"assignedByUid"}); ## order of the task $args{'item_order'} = $h->{"order"} if ($h->{"order"}); ## child order of the task $args{'child_order'} = $h->{"child_order"} if ($h->{"child_order"}); my $dataArr=$commandsStart.'"type":"item_add","temp_id":"'.$tempId.'","uuid":"'.$uuid.'","args":'.encode_json(\%args).$commandsEnd; Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".Dumper(%args); my $data= { token => $pwd, commands => $dataArr }; $param = { url => $apiUrl."sync", data => $data, tTitle => $title, method => "POST", wType => "create", parentId => $parentId, timeout => 7, header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } } else { map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"alreadythere"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); todoist_ErrorReadings($hash,"duplicate detected","duplicate detected"); } return undef; } # create Task sub todoist_CreateTask_old($$) { my ($hash,$cmd) = @_; my($a, $h) = parseParams($cmd); my $name=$hash->{NAME}; my $param; my $pwd=""; my $assigne_id=""; ## we try to send a due_date (in developement) my @tmp = split( ":", join(" ",@$a) ); my $title=encode_utf8($tmp[0]); $title = encode_utf8($h->{"title"}) if ($h->{"title"}); my $check=1; # we can avoid duplicates in FHEM. There may still come duplicates coming from another app if (AttrVal($name,"avoidDuplicates",0) == 1 && todoist_inArray(\@{$hash->{helper}{"TITS"}},$title)) { $check=-1; } if ($check==1) { ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); # data array for API - we could transfer more data my $data = { project_id => int($hash->{PID}), content => $title, token => $pwd, }; ## check for dueDate as Parameter or part of title - push to hash if (!$tmp[1] && $h->{"dueDate"}) { ## parameter $data->{'date_string'} = $h->{"dueDate"}; } elsif ($tmp[1]) { ## title $data->{'date_string'} = $tmp[1]; } else { } ## if someone uses due_date - no problem $data->{'date_string'} = $h->{"due_date"} if ($h->{"due_date"}); $data->{'date_string'} = encode_utf8($data->{'date_string'}); ## Task parent_id $data->{'parent_id'} = int($h->{"parent_id"}) if ($h->{"parent_id"}); $data->{'parent_id'} = int($h->{"parentID"}) if ($h->{"parentID"}); $data->{'parent_id'} = int($h->{"parentId"}) if ($h->{"parentId"}); my $parentId = 0; $parentId = $data->{'parent_id'} if ($data->{'parent_id'}); ## Task priority $data->{'priority'} = $h->{"priority"} if ($h->{"priority"}); ## who is responsible for the task? $data->{'responsible_uid'} = $h->{"responsibleUid"} if ($h->{"responsibleUid"}); $data->{'responsible_uid'} = $h->{"responsible"} if ($h->{"responsible"}); ## who assigned the task? $data->{'assigned_by_uid'} = $h->{"assignedByUid"} if ($h->{"assignedByUid"}); $data->{'assigned_by_uid'} = $h->{"assignedBy"} if ($h->{"assignedByUid"}); ## order of the task $data->{'item_order'} = $h->{"order"} if ($h->{"order"}); ## child order of the task $data->{'child_order'} = $h->{"child_order"} if ($h->{"child_order"}); Log3 $name,4, "todoist ($name): Data Array sent to todoist API: ".Dumper($data); $param = { url => $apiUrl."items/add", data => $data, tTitle => $title, method => "POST", wType => "create", parentId => $parentId, timeout => 7, header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_HandleTaskCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+0.1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } } else { map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_ErrorDialog === \"function\") todoist_ErrorDialog('$name','$title ".$todoist_tt->{"alreadythere"}."','".$todoist_tt->{"error"}."')", "")} devspec2array("TYPE=FHEMWEB"); todoist_ErrorReadings($hash,"duplicate detected","duplicate detected"); } return undef; } # handle the callback data if task was created or updated sub todoist_HandleTaskCallback($$$){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $title = $param->{tTitle}; my $taskId = 0; $taskId = $param->{taskId} if ($param->{taskId}); my $reading = $title; my $name = $hash->{NAME}; Log3 $name,4, "todoist ($name): ".$param->{wType}." - Task Callback data: ".Dumper($data); my $error; ## errors? Log and readings if ($err ne "") { todoist_ErrorReadings($hash,$err); } else { ## if "sync_status" in $data, we were successfull if((($data =~ /sync_status/ && $data=~/ok/) || $data =~ /sync_id/) && eval {decode_json($data)}) { readingsBeginUpdate($hash); if ($data ne "") { my $decoded_json = decode_json($data); $taskId = $decoded_json->{id} if ($decoded_json->{id}); $reading .= " - ".$taskId; ## do some logging Log3 $name,5, "todoist ($name): Task Callback data (decoded JSON): ".Dumper($decoded_json ); Log3 $name,4, "todoist ($name): Callback-ID: $taskId" if ($taskId); } Log3 $name,4, "todoist ($name): Task Callback error(s): ".Dumper($err) if ($err); Log3 $name,5, "todoist ($name): Task Callback param: ".Dumper($param); # set information readings readingsBulkUpdate($hash, "error","none"); readingsBulkUpdate($hash, "lastCreatedTask",$reading) if ($param->{wType} eq "create"); readingsBulkUpdate($hash, "lastCompletedTask",$reading) if ($param->{wType} eq "complete" || $param->{wType} eq "close"); readingsBulkUpdate($hash, "lastUncompletedTask",$reading) if ($param->{wType} eq "uncomplete"); readingsBulkUpdate($hash, "lastUpdatedTask",$reading) if ($param->{wType} eq "update"); readingsBulkUpdate($hash, "lastDeletedTask",$reading) if ($param->{wType} eq "delete"); ## some Logging Log3 $name, 4, "todoist ($name): successfully created new task $title" if ($param->{wType} eq "create"); Log3 $name, 4, "todoist ($name): success: ".$param->{wType}." task $title" if ($title); readingsEndUpdate( $hash, 1 ); if ($param->{wType} =~ /(complete|delete|close)/) { # remove line in possible webling widget map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_removeLine === \"function\") todoist_removeLine('$name','$taskId')", "")} devspec2array("TYPE=FHEMWEB"); } if ($param->{wType} eq "create") { if ($param->{parentId}) { # set parent id with additional updateTask command / API cannot add it in create CommandSet(undef, "$name moveTask ID:$taskId parent_id=".$param->{parentId}); Log3 $name, 3, "todoist ($name): startet set parent_id over update after create: Task-ID: ".$taskId." - parent_id: ".$param->{parentId}; } # add a line in possible weblink widget map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_addLine === \"function\") todoist_addLine('$name','$taskId','$title')", "")} devspec2array("TYPE=FHEMWEB"); } } ## we got an error from the API else { my $error="malformed JSON"; # if the error is in the file, log this error if (eval {decode_json($data)}) { my $decoded_json = decode_json($data); $error = $decoded_json->{error} if ($decoded_json->{error}); } $error .= " | ".$param->{wType}."Task: ".$taskId; $error .= "Unknown"; Log3 $name, 2, "todoist ($name): got error: ".$error; todoist_ErrorReadings($hash,$error); } } # restart the timers todoist_RestartGetTimer($hash); return undef; } ## get all Tasks sub todoist_GetTasks($;$) { my ($hash,$completed) = @_; my $name=$hash->{NAME}; # add loading circle to possivle weblink widget map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_addLoading === \"function\") todoist_addLoading('$name')", "")} devspec2array("TYPE=FHEMWEB"); $completed = 0 unless defined($completed); my $param; my $param2; my $pwd=""; ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); my $data= { token => $pwd, project_id => $hash->{PID} }; # set url for API access my $url = $apiUrl."projects/get_data"; ## check if we get also the completed Tasks if ($completed == 1) { $url = $apiUrl."completed/get_all"; $data->{'limit'}=50; } Log3 $name,4, "todoist ($name): Curl Data: ".Dumper($data); ## get the tasks $param = { url => $url, method => "POST", data => $data, header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, timeout => 7, completed => $completed, hash => $hash, callback => \&todoist_GetTasksCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+0.2, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } ## one more time, if we want to get completed tasks if (AttrVal($name,"getCompleted",0)==1 && $completed != 1) { InternalTimer(gettimeofday()+0.5, "todoist_doGetCompTasks", $hash, 0); } InternalTimer(gettimeofday()+0.3, "todoist_GetUsers", $hash, 0) if ($completed != 1 && AttrVal($name,"autoGetUsers",1) == 1); return undef; } # helper sub for getting completed tasks sub todoist_doGetCompTasks($) { my ($hash) = @_; todoist_GetTasks($hash,1); } ## Callback for the lists tasks sub todoist_GetTasksCallback($$$){ my ($param, $err, $data) = @_; my $hash=$param->{hash}; my $name = $hash->{NAME}; Log3 $name,5, "todoist ($name): Task Callback data-raw: ".Dumper($data); my $lText=""; Log3 $name,5, "todoist ($name): Task Callback param: ".Dumper($param); readingsBeginUpdate($hash); my $prefix="Task_"; ## Log possbile errors in callback if ($err ne "") { todoist_ErrorReadings($hash,$err); } else { my $decoded_json=""; # check for correct JSON if (eval{decode_json($data)}) { $decoded_json = decode_json($data); Log3 $name,5, "todoist ($name): Task Callback data (decoded JSON): ".Dumper($decoded_json ); } # mostly HTML response / todoist is down or locked if ((ref($decoded_json) eq "HASH" && !$decoded_json->{items}) || $decoded_json eq "") { $hash->{helper}{errorData} = Dumper($data); $hash->{helper}{errorMessage} = "GetTasks: Response was damaged or empty. See log for details."; InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); } # got project else { Log3 $name,4, "todoist ($name): getTasks was successful"; Log3 $name,5, "todoist ($name): Task item data: ".Dumper(@{$decoded_json->{items}}); ## items data my @taskseries = @{$decoded_json->{items}}; ## project data my $project = $decoded_json->{project}; # set some internals (project data) if ($project) { $hash->{PROJECT_NAME}=encode_utf8($project->{name}); $hash->{PROJECT_COLOR}=$project->{color}; $hash->{PROJECT_ORDER}=$project->{child_order}; if ($project->{user_id}) { $hash->{PROJECT_USER}=$project->{user_id}; } else { delete($hash->{PROJECT_USER}); } if ($project->{parent_id}) { $hash->{PROJECT_PARENT}=$project->{parent_id}; # hidden reading readingsBulkUpdate($hash, ".projectParentId",$project->{parent_id}); } else { # delete parent_id if there is none delete($hash->{PROJECT_PARENT}) if (defined($hash->{PROJECT_PARENT})); CommandDeleteReading(undef, ".projectParentId"); } } ## do some logging Log3 $name,5, "todoist ($name): Task Callback data (taskseries): ".Dumper(@taskseries ); my $i=0; ## count the results my $count=@taskseries; ## delete Task_* readings for changed list if ($param->{completed} != 1 || (ReadingsVal($name,"count",0)==0 && $count == 0)) { CommandDeleteReading(undef, "$hash->{NAME} (T|t)ask_.*"); delete($hash->{helper}); } #$prefix="cTask_" if ($param->{completed} == 1); ## no data if ($count==0 && $param->{completed} != 1) { InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); readingsBulkUpdate($hash, "count",0); } # got some tasks else { $i = ReadingsVal($name,"count",0) if ($param->{completed} == 1); foreach my $task (@taskseries) { my $title = encode_utf8($task->{content}); $title =~ s/^\s+|\s+$//g; my $t = sprintf ('%03d',$i); ## get todoist-Task-ID my $taskID = $task->{id}; $taskID = $task->{task_id} if ($param->{completed} == 1); readingsBulkUpdate($hash, $prefix.$t,$title); readingsBulkUpdate($hash, $prefix.$t."_ID",$taskID) if (AttrVal($name,"hideId",0)!=1); # convert title my $nTitle = $title; $nTitle=~s/ /_/g; ## a few helper for ID and revision $hash->{helper}{"IDS"}{"Task_".$i}=$taskID; # todoist Task-ID $hash->{helper}{"TITLE"}{$taskID}=$title; # Task title (content) $hash->{helper}{"TITLES"}{$nTitle}=$taskID; # Task title (content) $hash->{helper}{"WID"}{$taskID}=$i; # FHEM Task-ID $hash->{helper}{"parent_id"}{$taskID}=$task->{parent_id}; # parent_id of item $hash->{helper}{"section_id"}{$taskID}=$task->{section_id}; # section_id of item $hash->{helper}{"child_order"}{$taskID}=$task->{child_order}; # order of task under parent $hash->{helper}{"PRIORITY"}{$taskID}=$task->{priority}; # todoist Task priority #push @{$hash->{helper}{"PARENTS"}{$task->{parent_id}}},$taskID; # ident for better widget $hash->{helper}{"ORDER"}{$taskID}=$task->{item_order}; # todoist Task order if ($param->{completed} != 1) { push @{$hash->{helper}{"TIDS"}},$taskID; # simple ID list push @{$hash->{helper}{"TITS"}},$title; # simple ID list } readingsBulkUpdate($hash, $prefix.$t."_parent_id",$task->{parent_id}) if (AttrVal($name,"showParent",0)==1); readingsBulkUpdate($hash, $prefix.$t."_order",$task->{item_order}) if (AttrVal($name,"showOrder",0)==1); ## set parent_id if not null if (defined($task->{parent_id}) && $task->{parent_id} ne 'null') { ## if this task has a parent_id we set the reading readingsBulkUpdate($hash, $prefix.$t."_parentID",$task->{parent_id}) if (AttrVal($name,"showParent",1)==1); $hash->{helper}{"PARENT_ID"}{$taskID}=$task->{parent_id}; } ## set section_id if not null if (defined($task->{section_id}) && $task->{section_id} ne 'null') { ## if this task has a parent_id we set the reading readingsBulkUpdate($hash, $prefix.$t."_sectionID",$task->{section_id}) if (AttrVal($name,"showSection",1)==1); $hash->{helper}{"SECTION_ID"}{$taskID}=$task->{section_id}; } ## set completed_date if present if (defined($task->{checked}) && $task->{checked}!=0) { readingsBulkUpdate($hash, $prefix.$t."_checked",$task->{checked}) if (AttrVal($name,"showChecked",1)==1); $hash->{helper}{"CHECKED"}{$taskID}=$task->{checked}; } ## set completed_date if present if (defined($task->{is_deleted}) && $task->{is_deleted}!=0) { readingsBulkUpdate($hash, $prefix.$t."_isDeleted",$task->{is_deleted}) if (AttrVal($name,"showDeleted",1)==1); $hash->{helper}{"ISDELETED"}{$taskID}=$task->{is_deleted}; } ## set completed_date if present if (defined($task->{completed_date})) { ## if there is a completed task, we create a new reading readingsBulkUpdate($hash, $prefix.$t."_completedAt",FmtDateTime(str2time($task->{completed_date}))); $hash->{helper}{"COMPLETED_AT"}{$taskID}=FmtDateTime(str2time($task->{completed_date})); readingsBulkUpdate($hash, $prefix.$t."_completedById",$task->{user_id}); $hash->{helper}{"COMPLETED_BY_ID"}{$taskID}=$task->{user_id}; } ## set due_date if present if (defined($task->{due}) && $task->{due}{date} ne 'null') { ## if there is a task with due date, we create a new reading readingsBulkUpdate($hash, $prefix.$t."_dueDate",FmtDateTime(str2time($task->{due}{date}))); $hash->{helper}{"DUE_DATE"}{$taskID}=FmtDateTime(str2time($task->{due}{date})); } ## set responsible_uid if present if (defined($task->{responsible_uid})) { ## if there is a task with responsible_uid, we create a new reading readingsBulkUpdate($hash, $prefix.$t."_responsibleUid",$task->{responsible_uid}) if (AttrVal($name,"showResponsible",0)==1); $hash->{helper}{"RESPONSIBLE_UID"}{$taskID}=$task->{responsible_uid}; } ## set assigned_by_uid if present if (defined($task->{assigned_by_uid})) { ## if there is a task with assigned_by_uid, we create a new reading readingsBulkUpdate($hash, $prefix.$t."_assignedByUid",$task->{assigned_by_uid}) if (AttrVal($name,"showAssignedBy",0)==1); $hash->{helper}{"ASSIGNEDBY_UID"}{$taskID}=$task->{assigned_by_uid}; } ## set priority if present if (defined($task->{priority})) { readingsBulkUpdate($hash, $prefix.$t."_priority",$task->{priority}) if (AttrVal($name,"showPriority",0)==1); $hash->{helper}{"PRIORITY"}{$taskID}=$task->{priority}; } ## set recurrence_type and count if present if (defined($task->{date_string})) { ## if there is a task with recurrence_type, we create new readings readingsBulkUpdate($hash, $prefix.$t."_recurrenceType",encode_utf8($task->{date_string})); $hash->{helper}{"RECURRENCE_TYPE"}{$taskID}=encode_utf8($task->{date_string}); } if ($param->{completed} != 1) { $lText.=AttrVal($name,"listDivider",", ") if ($i != 0); $lText.=$title; } $i++; } readingsBulkUpdate($hash, "error","none"); readingsBulkUpdate($hash, "count",$i); } } } readingsEndUpdate( $hash, 1 ); ## list Text for TTS, Text-Message... if ($param->{completed} != 1) { $lText="-" if ($lText eq ""); readingsSingleUpdate($hash,"listText",$lText,1) if ($lText ne ""); } RemoveInternalTimer($hash,"todoist_GetTasks"); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "todoist_GetTasks", $hash, 0); ## loop with Interval todoist_ReloadTable($name); return undef; } ## get all Users sub todoist_GetUsers($) { my ($hash) = @_; my $name=$hash->{NAME}; my $param; my $pwd=""; ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); my $data= { token => $pwd, sync_token => '*', resource_types => '["collaborators"]' }; if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); $param = { url => $apiUrl."sync", data => $data, timeout => 7, method => "POST", header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_GetUsersCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } return undef; } sub todoist_GetUsersCallback($$$){ my ($param, $err, $data) = @_; my $hash=$param->{hash}; my $name = $hash->{NAME}; Log3 $name,5, "todoist ($name): User Callback data: ".Dumper($data); if ($err ne "") { todoist_ErrorReadings($hash,$err); } else { my $decoded_json=""; if (eval{decode_json($data)}) { $decoded_json = decode_json($data); Log3 $name,5, "todoist ($name): User Callback data (decoded JSON): ".Dumper($decoded_json ); } readingsBeginUpdate($hash); if ((ref($decoded_json) eq "HASH" && !$decoded_json->{collaborators}) || $decoded_json eq "") { $hash->{helper}{errorData} = Dumper($data); $hash->{helper}{errorMessage} = "Response was damaged or empty. See log for details."; InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); } else { my @users = @{$decoded_json->{collaborators}}; my @states = @{$decoded_json->{collaborator_states}}; ## count the results my $count=@users; ## delete Task_* readings for changed list CommandDeleteReading(undef, "$hash->{NAME} (U|u)ser_.*"); delete($hash->{helper}{USER}); Log3 $name,5, "todoist ($name): Task States: ".Dumper(@states); ## no data if ($count==0) { readingsBulkUpdate($hash, "error","none"); readingsBulkUpdate($hash, "countUsers",0); } else { my $i=0; foreach my $user (@users) { my $do=0; foreach my $state (@states) { $do=1 if ($user->{id} == $state->{user_id} && $state->{project_id} == $hash->{PID}); } if ($do==1) { my $userName = encode_utf8($user->{full_name}); my $t = sprintf ('%03d',$i); ## get todoist-User-ID my $userID = $user->{id}; readingsBulkUpdate($hash, "User_".$t,$userName); readingsBulkUpdate($hash, "User_".$t."_ID",$userID); ## a few helper for ID and revision $hash->{helper}{USER}{"IDS"}{"User_".$i}=$userID; $hash->{helper}{USER}{"NAME"}{$userID}=$userName; $hash->{helper}{USER}{"WID"}{$userID}=$i; $i++; } } readingsBulkUpdate($hash, "error","none"); readingsBulkUpdate($hash, "countUsers",$i); } } readingsEndUpdate( $hash, 1 ); todoist_ReloadTable($name); } } # get all projects sub todoist_GetProjects($) { my ($hash) = @_; my $name=$hash->{NAME}; my $param; my $pwd=""; ## if no token is needed and device is not disabled, check token and get list vom todoist if (!$hash->{helper}{PWD_NEEDED} && !IsDisabled($name)) { ## get password $pwd=todoist_GetPwd($hash); my $data= { token => $pwd, sync_token => '*', resource_types => '["projects"]' }; if ($pwd) { Log3 $name,5, "$name: hash: ".Dumper($hash); $param = { url => $apiUrl."sync", data => $data, timeout => 7, method => "POST", header => "Content-Type: application/x-www-form-urlencoded\r\n". "Authorization: Bearer ".$pwd, hash => $hash, callback => \&todoist_GetProjectsCallback, ## call callback sub to work with the data we get }; Log3 $name,5, "todoist ($name): Param: ".Dumper($param); ## non-blocking access to todoist API InternalTimer(gettimeofday()+1, "HttpUtils_NonblockingGet", $param, 0); } else { todoist_ErrorReadings($hash,"access token empty"); } } else { if (!IsDisabled($name)) { todoist_ErrorReadings($hash,"no access token set"); } else { todoist_ErrorReadings($hash,"device is disabled"); } } return undef; } # get projects callback sub todoist_GetProjectsCallback($$$){ my ($param, $err, $data) = @_; my $hash=$param->{hash}; my $name = $hash->{NAME}; ## some Project-Info my $pid = $hash->{PID}; my $room = AttrVal($name,"room",undef); my $group = AttrVal($name,"group",undef); my $return=""; my $i=0; Log3 $name,5, "todoist ($name): Projects Callback data: ".Dumper($data); readingsBeginUpdate($hash); if ($err ne "") { todoist_ErrorReadings($hash,$err); } else { my $decoded_json=""; if (eval{decode_json($data)}) { $decoded_json = decode_json($data); Log3 $name,5, "todoist ($name): User Callback data (decoded JSON): ".Dumper($decoded_json ); } if ((ref($decoded_json) eq "HASH" && !$decoded_json->{projects}) || $decoded_json eq "") { $hash->{helper}{errorData} = Dumper($data); $hash->{helper}{errorMessage} = "GetTasks: Response was damaged or empty. See log for details."; InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); } else { Log3 $name,4, "todoist ($name): getProjects was successful"; Log3 $name,5, "todoist ($name): Task projects data: ".Dumper(@{$decoded_json->{projects}}); ## items data my @projects = @{$decoded_json->{projects}}; ## count the results my $count=@projects; ## no data if ($count==0) { InternalTimer(gettimeofday()+0.2, "todoist_ErrorReadings",$hash, 0); readingsBulkUpdate($hash, "count",0); } else { # array for possible deletion my @comDevs; # walk through projects foreach my $project (@projects) { my $project_id = $project->{id}; my $parent_id = $project->{parent_id}; my $new_name = encode_utf8($project->{name}); # new title my $title = $name."_".$new_name; $title =~ s!\s!!g; # is this parent_id equal to id of current project? if ($pid == $parent_id) { # push to deletion array push @comDevs,$project_id; Log3 $name,4, "todoist ($name): get Projects: Project-ID: $project_id - Title: $new_name - Parent-ID: $parent_id"; # does this project exist in FHEM? my $existP = devspec2array("PID=$project_id"); if (!$existP && (!$project->{is_deleted} || $project->{is_deleted}!=1)) { # if not, define new todoist device by copying the parent fhem("copy $name $title $project_id") if (!defined($defs{$title})); my $new_hash = $defs{$title}; # set some internals (project data) and set parent_id if ($new_hash) { $return.=" $title"; $i++; Log3 $name,4, "todoist ($name): new project $title, defined by cChildProjects"; $new_hash->{PROJECT_NAME}=$project->{name}; $new_hash->{PROJECT_COLOR}=$project->{color}; $new_hash->{PROJECT_ORDER}=$project->{child_order}; if ($project->{user_id}) { $new_hash->{PROJECT_USER}=$project->{user_id}; } else { delete($new_hash->{PROJECT_USER}); } if ($project->{parent_id}) { $new_hash->{PROJECT_PARENT}=$project->{parent_id}; # invisible reading readingsSingleUpdate($new_hash, ".projectParentId",$project->{parent_id},1); } else { delete($new_hash->{PROJECT_PARENT}) if (defined($new_hash->{PROJECT_PARENT})); CommandDeleteReading(undef, ".projectParentId"); } } } } } if (AttrVal($name,"delDeletedLists",0)==1) { # get todoist projects for this parent_id my @tDevs = devspec2array(".projectParentId=$pid"); foreach my $dev (@tDevs) { my $tDev = $defs{$dev}; # delete device if PID is not in array if (!todoist_inArray(\@comDevs,$tDev->{PID})) { CommandDelete(undef,$dev); Log3 $name,3, "todoist ($name): deleted device ".$dev." in cChildProjects"; } } } } } } Log3 $name,3, "todoist ($name): Got $i new projects: ".$return; return "Got $i new projects: ".$return; } ################################################# # delete all Tasks from list sub todoist_clearList($) { my ($hash) = @_; my $name = $hash->{NAME}; my $i=0; ## iterate through all tasks foreach my $id (%{$hash->{helper}{IDS}}) { my $dHash->{hash}=$hash; if ($id !~ /Task_/) { $dHash->{id}=$id; InternalTimer(gettimeofday()+0.4, "todoist_doUpdateTask", $dHash, 0); $i++; } } if ($i==0) { map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_removeLoading === \"function\") todoist_removeLoading('$name')", "")} devspec2array("TYPE=FHEMWEB"); } } sub todoist_doUpdateTask($) { my ($dHash) = @_; my $hash = $dHash->{hash}; my $id = $dHash->{id}; my $name = $hash->{NAME}; todoist_UpdateTask($hash,"ID:".$id,"delete"); } sub todoist_Undefine($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); return undef; } ################################################ # If Device is deleted, delete the password data sub todoist_Delete($$) { my ($hash, $name) = @_; my $old_index = "todoist_".$name."_passwd"; my $old_key =getUniqueId().$old_index; my ($err, $old_pwd) = getKeyValue($old_index); return undef unless(defined($old_pwd)); setKeyValue($old_index, undef); Log3 $name, 3, "todoist: device $name as been deleted. Access-Token has been deleted too."; return undef; } ################################################ # If Device is renamed, copy the password data sub todoist_Rename($$) { my ($new, $old) = @_; my $old_index = "todoist_".$old."_passwd"; my $new_index = "todoist_".$new."_passwd"; my $key = getUniqueId().$old_index; my $new_hash = $defs{$new}; my $name = $new_hash->{NAME}; my $old_key=""; my ($err, $password) = getKeyValue($old_index); if ($err) { $new_hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; return undef; } if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $old_key.=chr(ord($char)^ord($decode)); $key=$decode.$key; } } my $new_key = $old_key; return undef unless(defined($old_key)); todoist_setPwd($new_hash, $name, $new_key); Log3 $new, 3, "todoist: device has been renamed from $old to $new. Access-Token has been assigned to new name."; return undef; } ################################################ # If Device is copied, copy the password data sub todoist_Copy($$;$) { my ($old, $new) = @_; my $old_index = "todoist_".$old."_passwd"; my $new_index = "todoist_".$new."_passwd"; my $key = getUniqueId().$old_index; my $new_hash = $defs{$new}; my $name = $new_hash->{NAME}; my $old_key=""; my ($err, $password) = getKeyValue($old_index); if ($err) { $new_hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 4, "todoist ($name): unable to read password from file: $err"; return undef; } if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $old_key.=chr(ord($char)^ord($decode)); $key=$decode.$key; } } my $new_key = $old_key; return undef unless(defined($old_key)); todoist_setPwd($new_hash, $name, $new_key); delete($new_hash->{helper}{PWD_NEEDED}); readingsSingleUpdate($new_hash,"state","inactive",1); Log3 $new, 3, "todoist: device has been copied from $old to $new. Access-Token has been assigned to new device."; return undef; } ## some checks if attribute is set or deleted sub todoist_Attr($@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; my $hash = $defs{$name}; if ( $attrName eq "disable" ) { if ( $cmd eq "set" && $attrVal == 1 ) { if ($hash->{READINGS}{state}{VAL} ne "disabled") { readingsSingleUpdate($hash,"state","disabled",1); RemoveInternalTimer($hash); RemoveInternalTimer($hash,"todoist_GetTasks"); Log3 $name, 4, "todoist ($name): $name is now disabled"; } } elsif ( $cmd eq "del" || $attrVal == 0 ) { if ($hash->{READINGS}{state}{VAL} ne "active") { readingsSingleUpdate($hash,"state","active",1); RemoveInternalTimer($hash); Log3 $name, 4, "todoist ($name): $name is now ensabled"; todoist_RestartGetTimer($hash); } } } if ( $attrName eq "pollInterval") { if ( $cmd eq "set" ) { return "$name: pollInterval has to be a number (seconds)" if ($attrVal!~ /\d+/); return "$name: pollInterval has to be greater than or equal 20" if ($attrVal < 20); $hash->{INTERVAL}=$attrVal; Log3 $name, 4, "todoist ($name): set new pollInterval to $attrVal"; } elsif ( $cmd eq "del" ) { $hash->{INTERVAL}=1800; Log3 $name, 4, "todoist ($name): set new pollInterval to 1800 (standard)"; } todoist_RestartGetTimer($hash); } if ($attrName eq "listDivider") { todoist_RestartGetTimer($hash); } if ($attrName eq "language") { # in any attribute redefinition readjust language if ($cmd eq "set") { return "$name: language can only be DE or EN" if ($attrVal !~ /(^DE|EN)$/); if( $attrVal eq "DE") { $todoist_tt = \%todoist_transtable_DE; } else{ $todoist_tt = \%todoist_transtable_EN; } } else { $todoist_tt = \%todoist_transtable_EN; } } if ( $attrName =~ /(show(Priority|AssignedBy|Responsible|Order|DetailWidget|Section|Parent)|getCompleted|hide(Id|ListIfEmpty)|autoGetUsers|avoidDuplicates|delDeletedLists)/) { if ( $cmd eq "set" ) { return "$name: $attrName has to be 0 or 1" if ($attrVal !~ /^(0|1)$/); Log3 $name, 4, "todoist ($name): set attribut $attrName to $attrVal"; } elsif ( $cmd eq "del" ) { Log3 $name, 4, "todoist ($name): deleted attribut $attrName"; } todoist_RestartGetTimer($hash); } return; } sub todoist_Set ($@) { my ($hash, $name, $cmd, @args) = @_; my @sets = (); push @sets, "active:noArg" if (IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); push @sets, "inactive:noArg" if (!IsDisabled($name)); if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}) { push @sets, "addTask"; push @sets, "completeTask"; push @sets, "closeTask"; push @sets, "uncompleteTask"; push @sets, "deleteTask"; push @sets, "updateTask"; push @sets, "moveTask"; push @sets, "clearList:noArg"; push @sets, "getTasks:noArg"; push @sets, "cChildProjects:noArg"; push @sets, "getUsers:noArg"; push @sets, "reorderTasks"; } push @sets, "accessToken" if ($hash->{helper}{PWD_NEEDED}); push @sets, "newAccessToken" if (!$hash->{helper}{PWD_NEEDED}); return join(" ", @sets) if ($cmd eq "?"); my $usage = "Unknown argument ".$cmd.", choose one of ".join(" ", @sets) if(scalar @sets > 0); if (IsDisabled($name) && $cmd !~ /^(active|inactive|.*ccessToken)?$/) { Log3 $name, 3, "todoist ($name): Device is disabled at set Device $cmd"; return "Device is disabled. Enable it on order to use command ".$cmd; } if ( $cmd =~ /^(active|inactive)?$/ ) { readingsSingleUpdate($hash,"state",$cmd,1); RemoveInternalTimer($hash,"todoist_GetTasks"); CommandDeleteAttr(undef,"$name disable") if ($cmd eq "active" && AttrVal($name,"disable",0)==1); InternalTimer(gettimeofday()+1, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && $cmd eq "active"); Log3 $name, 3, "todoist ($name): set Device $cmd"; } elsif ($cmd eq "getTasks") { RemoveInternalTimer($hash,"todoist_GetTasks"); Log3 $name, 4, "todoist ($name): set getTasks manually. Timer restartet."; InternalTimer(gettimeofday()+1, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); } elsif ($cmd eq "cChildProjects") { RemoveInternalTimer($hash,"todoist_GetProjects"); Log3 $name, 4, "todoist ($name): set getProjects manually. Timer restartet."; InternalTimer(gettimeofday()+1, "todoist_GetProjects", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); } elsif ($cmd eq "getUsers") { RemoveInternalTimer($hash,"todoist_GetUsers"); Log3 $name, 4, "todoist ($name): set getUsers manually."; InternalTimer(gettimeofday()+1, "todoist_GetUsers", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); } elsif ($cmd eq "accessToken" || $cmd eq "newAccessToken") { return todoist_setPwd ($hash,$name,@args); } elsif ($cmd eq "addTask" || $cmd eq "newTask") { my $count=@args; if ($count!=0) { my $exp=decode_utf8(join(" ",@args)); todoist_CreateTask ($hash,$exp); } return "new Task needs a title" if ($count==0); } elsif ($cmd eq "completeTask" || $cmd eq "closeTask") { my $term=$cmd eq "completeTask"?"complete":"close"; my $count=@args; if ($count!=0) { my $exp=decode_utf8(join(" ",@args)); Log3 $name,5, "todoist ($name): ".$term."d startet with exp: $exp"; todoist_UpdateTask ($hash,$exp,$term); } return "in order to complete or close a task, we need it's ID" if ($count==0); } elsif ($cmd eq "uncompleteTask") { my $count=@args; if ($count!=0) { my $exp=decode_utf8(join(" ",@args)); Log3 $name,5, "todoist ($name): Uncompleted startet with exp: $exp"; todoist_UpdateTask ($hash,$exp,"uncomplete"); } return "in order to complete a task, we need it's ID" if ($count==0); } elsif ($cmd eq "updateTask" || $cmd eq "moveTask") { my $term=$cmd eq "updateTask"?"update":"move"; my $count=@args; if ($count!=0) { my $exp=decode_utf8(join(" ",@args)); todoist_UpdateTask ($hash,$exp,$term); } return "in order to complete a task, we need it's ID" if ($count==0); } elsif ($cmd eq "reorderTasks") { my $count=@args; if ($count!=0) { my $exp=$args[0]; todoist_ReorderTasks ($hash,$exp); } return "in order to delete a task, we need it's ID" if ($count==0); } elsif ($cmd eq "deleteTask") { my $count=@args; if ($count!=0) { my $exp=decode_utf8(join(" ",@args)); todoist_UpdateTask ($hash,$exp,"delete"); } return "in order to delete a task, we need it's ID" if ($count==0); } elsif ($cmd eq "sortTasks") { todoist_sort($hash); } elsif ($cmd eq "clearList") { todoist_clearList($hash); } else { return $usage; } return undef; } sub todoist_Get($@) { my ($hash, $name, $cmd, @args) = @_; my $ret = undef; if ( $cmd eq "version") { $hash->{VERSION} = $version; return "Version: ".$version; } else { $ret ="$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets); } return $ret; } ##################################### # sets todoist Access Token sub todoist_setPwd($$@) { my ($hash, $name, @pwd) = @_; return "todoist: Password can't be empty" if (!@pwd); my $pwdString=$pwd[0]; my $enc_pwd = ""; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (split //, $pwdString) { my $encode=chop($key); $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } Log3 $name,5,"todoist ($name): encoded pwd: $enc_pwd"; my $err = setKeyValue($index, $enc_pwd); return "error while saving the password - $err" if(defined($err)); delete($hash->{helper}{PWD_NEEDED}) if(exists($hash->{helper}{PWD_NEEDED})); if (AttrVal($name,"disable",0) != 1) { readingsSingleUpdate($hash,"state","active",1); } todoist_RestartGetTimer($hash); Log3 $name, 3, "todoist ($name). New Password set."; return "password successfully saved"; } ##################################### # reads the Access Token and checks it sub todoist_checkPwd ($$) { my ($hash, $pwd) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my ($err, $password) = getKeyValue($index); if ($err) { $hash->{helper}{PWD_NEEDED} = 1; Log3 $name, 3, "todoist ($name): unable to read password from file: $err"; return undef; } if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } my $dec_pwd = ''; for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $dec_pwd.=chr(ord($char)^ord($decode)); $key=$decode.$key; } return 1 if ($dec_pwd eq $pwd); } else { return "no password saved" if (!$password); } return 0; } sub todoist_Notify ($$) { my ($hash,$dev) = @_; my $name = $hash->{NAME}; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); todoist_RestartGetTimer($hash); return undef; } # restart timers for getTasks if active sub todoist_RestartGetTimer($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash, "todoist_GetTasks"); InternalTimer(gettimeofday()+0.3, "todoist_GetTasks", $hash, 0) if (!IsDisabled($name) && !$hash->{helper}{PWD_NEEDED}); return undef; } # placeholder for older widgets sub todoist_AllHtml(;$) { my ($regEx) = @_; return todoist_Html($regEx); } # show widget in detail view of todoist device sub todoist_detailFn(){ my ($FW_wname, $devname, $room, $pageHash) = @_; # pageHash is set for summaryFn. my $hash = $defs{$devname}; $hash->{mayBeVisible} = 1; my $name=$hash->{NAME}; return undef if (IsDisabled($name) || AttrVal($name,"showDetailWidget",1)!=1); my $html=""; my $icon = AttrVal($devname, "icon", ""); $icon = FW_makeImage($icon,$icon,"icon") . " " if($icon); #$html = '
'. # ''. # ''. # '
'.$icon.' '.AttrVal($name,"alias",$name).'
'.InternalVal($devname,"STATE",ReadingsVal($devname,"state","-")).'
'; $html .= todoist_Html($name,undef,1); return $html; } # frontend weblink widget (FHEMWEB) sub todoist_Html(;$$$) { my ($regEx,$refreshGet,$detail) = @_; $regEx="" if (!defined($regEx)); $refreshGet=0 if (!defined($refreshGet)); $detail=0 if (!defined($detail)); my $filter=""; $filter.=":FILTER=".$regEx; my @devs = devspec2array("TYPE=todoist".$filter); my $ret=""; my $rot=""; my $eo=""; my $r=0; my $width = 95; my $count = @devs; $width = $width/$count if ($count>=1); # refresh request? don't return everything if (!$refreshGet) { # define global JS variables $rot .= " "; # Javascript and CSS $rot .= " "; $ret .= "
\n"; } foreach my $name (@devs) { $r++; my $hash = $defs{$name}; my $id = $defs{$name}{NR}; my $countList = ReadingsVal($name,"count",0); my $hLIE = AttrVal($name,"hideListIfEmpty",0); my $showList = ($hLIE==1 && $countList==0)?0:1; # show active lists only if (!IsDisabled($name) && ($showList || $detail)) { # refresh request? don't return everything if (!$refreshGet) { $ret .= "\n"; $ret .= "\n". " \n". "\n"; $ret .= "\n"; $ret .= "
\n". " \n". "
{PID}."\">\n"; } my $i=1; my $cs=3; # show data foreach (@{$hash->{helper}{TIDS}}) { if ($i%2==0) { $eo="even"; } else { $eo="odd"; } my $ind=0; my $dueDate = defined($hash->{helper}{DUE_DATE}{$_})?$hash->{helper}{DUE_DATE}{$_}:""; my $responsibleUid = defined($hash->{helper}{RESPONSIBLE_UID}{$_})?$hash->{helper}{RESPONSIBLE_UID}{$_}:""; $responsibleUid = $hash->{helper}{USER}{NAME}{$responsibleUid} if ($responsibleUid ne "" && defined($hash->{helper}{USER}{NAME}{$responsibleUid})); my $dueDateClass = $dueDate ne ""?" todoist_dueDate":""; my $responsibleUidClass = $responsibleUid ne ""?" todoist_responsibleUid":""; $ret .= "{PID}."\" data-line-id=\"".$_."\" class=\"sortit todoist_data ".$eo."\">\n". " \n". " \n"; $ret .= "\n"; $ret .= "\n"; $i++; } # refresh request? don't return everything if (!$refreshGet) { my $showPH = 0; $showPH = 1 if ($i==1); $ret .= ""; $ret .= " "; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
\n". "
\n". " {'check'}."\" class=\"todoist_checkbox_".$name."\" type=\"checkbox\" id=\"check_".$_."\" data-id=\"".$_."\" />\n". "
\n". " ".$hash->{helper}{TITLE}{$_}."\n". " {helper}{TITLE}{$_}."\" />\n". "
". "
". "
\n". " {'delete'}."\" href=\"#\" class=\"todoist_delete_".$name."\" data-id=\"".$_."\">\n". " x\n". " \n". "
". " ".$todoist_tt->{'nolistdata'}.".\n". "
". " \n". " {'newentry'}."\" type=\"text\" id=\"newEntry_".$name."\" />\n". "
\n"; } } } # refresh request? don't return everything if (!$refreshGet) { $ret .= "
\n"; $ret .= "
"; } return $rot.$ret; } # called if weblink widget table has to be updated sub todoist_ReloadTable($) { my ($name) = @_; my $ret = todoist_Html($name,1); $ret =~ s/\"/\'/g; $ret =~ s/\n//g; map {FW_directNotify("#FHEMWEB:$_", "if (typeof todoist_reloadTable === \"function\") todoist_reloadTable('$name',\"$ret\")", "")} devspec2array("TYPE=FHEMWEB"); } # check if element is in array sub todoist_inArray { my ($arr,$search_for) = @_; foreach (@$arr) { return 1 if ($_ eq $search_for); } return 0; } sub todoist_genUUID() { srand(gettimeofday()) if(!$srandUsed); $srandUsed = 1; my $uuid = sprintf("%08x-f33f-%s-%s-%s", time(), substr(getUniqueId(),-4), join("",map { unpack "H*", chr(rand(256)) } 1..2), join("",map { unpack "H*", chr(rand(256)) } 1..8)); return $uuid; } 1; =pod =item device =item summary uses todoist API to add, read, complete and delete tasks in a todoist tasklist =item summary_DE Taskverwaltung einer todoist Taskliste über die todoist API =begin html

todoist

=end html =cut