diff --git a/fhem/98_todoist.pm b/fhem/98_todoist.pm new file mode 100644 index 000000000..e599b3138 --- /dev/null +++ b/fhem/98_todoist.pm @@ -0,0 +1,2792 @@ +############################################## +# $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 \ No newline at end of file